Skip to main content
Orders in BeanQuick follow a structured lifecycle with distinct states for both order fulfillment and payment processing. This guide explains each state, transitions, and the actors involved.

Lifecycle Overview

BeanQuick uses two separate status fields: estado for order fulfillment and estado_pago for payment status.

Order States (estado)

The estado field tracks the order’s fulfillment progress:
Pedido.php:15-16
'estado',       // 'pendiente', 'pagado', 'preparando', 'listo', 'entregado', 'cancelado'
'estado_pago',  // 'pendiente', 'aprobado', 'rechazado'

Pendiente

Initial state when order is createdCharacteristics:
  • Order exists in database but not confirmed
  • Payment status: pendiente
  • Stock is NOT deducted yet
  • Customer can still cancel
  • Business cannot see this order yet
Created by:
PedidoController.php:97-105
$pedido = Pedido::create([
    'empresa_id'    => $request->empresa_id,
    'user_id'       => $user->id,
    'estado'        => 'Pendiente',
    'hora_recogida' => $request->hora_recogida,
    'total'         => $total,
    'estado_pago'   => 'pendiente'
]);
Stock validation occurs at this stage, but stock is NOT deducted until payment is approved.
Active preparation by businessCharacteristics:
  • Payment approved (estado_pago = 'aprobado')
  • Stock has been deducted
  • Order appears in business dashboard
  • Business is preparing the products
  • Customer cannot cancel (must contact business)
Transition: Automatically moves from Pendiente to Preparando when payment is approved via Mercado Pago webhook.Business sees:
PedidoController.php:153-157
$pedidos = Pedido::where('empresa_id', $empresa->id)
    ->where('estado_pago', 'aprobado')
    ->with(['productos', 'cliente'])
    ->get();
Ready for pickupCharacteristics:
  • Order is prepared and waiting for customer
  • Customer should arrive at scheduled pickup time
  • Business notifies customer (via frontend UI)
Updated by business:
PATCH /api/empresa/pedidos/{id}/estado
{ "estado": "Listo" }
This is the customer’s signal that their order is ready to collect.
Order completedCharacteristics:
  • Customer received their order
  • Transaction complete
  • Customer can now review products
  • Final state (terminal)
Updated by business:
PATCH /api/empresa/pedidos/{id}/estado
{ "estado": "Entregado" }
Once delivered, the order cannot be modified further.
Order cancelledCharacteristics:
  • Order will not be fulfilled
  • If payment was approved, stock is returned
  • Payment status changes to rechazado
  • Terminal state
Can be cancelled by:
  • Customer (only if estado = 'Pendiente')
  • Business (at any time via status update)
Cancellation logic:
PedidoController.php:179-192
\DB::transaction(function () use ($pedido) {
    // Only return stock if payment was approved
    if ($pedido->estado_pago === 'aprobado') {
        foreach ($pedido->productos as $producto) {
            $producto->increment('stock', $producto->pivot->cantidad);
        }
    }
    
    $pedido->update([
        'estado' => 'Cancelado',
        'estado_pago' => 'rechazado'
    ]);
});

Payment States (estado_pago)

Payment status is managed separately via Mercado Pago integration:
Awaiting payment
  • Order created but not yet paid
  • Customer must complete payment
  • Stock is validated but NOT deducted
  • Order not visible to business yet
Payment initiated:
POST /api/cliente/pedidos/{id}/pagar
This creates a Mercado Pago preference and returns a payment link.

Order Creation Flow

1

Customer Adds to Cart

Products from a single business are added to cart:
POST /api/cliente/carrito/agregar/{productoId}
{ "cantidad": 2 }
2

Create Order

Customer proceeds to checkout:
POST /api/cliente/pedidos
{
  "empresa_id": 1,
  "hora_recogida": "15:30"
}
System validates:
  • Cart is not empty
  • All products belong to specified business
  • Stock is sufficient for each product
  • No duplicate pending orders exist
3

Prevent Duplicates

PedidoController.php:39-49
$pedidoExistente = Pedido::where('user_id', $user->id)
    ->where('estado', 'Pendiente')
    ->where('estado_pago', 'pendiente')
    ->first();

if ($pedidoExistente) {
    return response()->json([
        'message' => 'Ya tienes un pedido pendiente de pago.'
    ], 200);
}
Customers can only have one pending unpaid order at a time.
4

Stock Validation

PedidoController.php:77-85
foreach ($productosTienda as $producto) {
    $cantidadPedida = $producto->pivot->cantidad ?? 1;
    
    if ($producto->stock < $cantidadPedida) {
        return response()->json([
            'message' => "Stock insuficiente para: {$producto->nombre}"
        ], 422);
    }
}
If any product is out of stock, the entire order is rejected.
5

Calculate Total

PedidoController.php:88-94
$total = 0;
foreach ($productosTienda as $producto) {
    $cantidad = $producto->pivot->cantidad ?? 1;
    $precio   = $producto->precio ?? 0;
    $total   += $precio * $cantidad;
}
6

Create Order Record

Order created with estado = 'Pendiente' and estado_pago = 'pendiente'Products are linked via pedido_productos pivot table with snapshot of price:
PedidoController.php:110-116
PedidoProducto::create([
    'pedido_id'       => $pedido->id,
    'producto_id'     => $producto->id,
    'cantidad'        => $producto->pivot->cantidad ?? 1,
    'precio_unitario' => $producto->precio ?? 0,
]);
Prices are stored at order time to preserve historical accuracy even if product prices change later.
7

Return Order

{
  "message": "Pedido generado pendiente de pago.",
  "pedido": {
    "id": 42,
    "empresa_id": 1,
    "user_id": 5,
    "estado": "Pendiente",
    "estado_pago": "pendiente",
    "hora_recogida": "15:30",
    "total": 45.50,
    "productos": [...]
  }
}

Payment Processing Flow

1

Initiate Payment

Customer clicks “Pay” on their pending order:
POST /api/cliente/pedidos/{id}/pagar
This generates a Mercado Pago payment link and returns it to the frontend.
2

Customer Pays

Customer is redirected to Mercado Pago to complete payment using:
  • Credit/debit card
  • Cash payment locations
  • Bank transfer
  • Other payment methods
3

Webhook Notification

Mercado Pago sends payment confirmation:
POST /api/webhook/mercadopago
{
  "type": "payment",
  "data": { "id": "payment_id" }
}
The PagoController processes this and:
  1. Verifies payment status
  2. Updates estado_pago = 'aprobado'
  3. Changes estado to Preparando
  4. Deducts stock from products
4

Order Activated

Order now appears in business dashboard for fulfillment
Critical: Stock is only deducted AFTER payment approval, not at order creation. This prevents stock being locked by unpaid orders.

Business Fulfillment Flow

1

View Orders

Business checks incoming orders:
GET /api/empresa/pedidos
Only shows orders where estado_pago = 'aprobado':
PedidoController.php:153-157
$pedidos = Pedido::where('empresa_id', $empresa->id)
    ->where('estado_pago', 'aprobado')
    ->with(['productos', 'cliente'])
    ->orderBy('created_at', 'asc')
    ->get();
2

Prepare Order

Order status is Preparando - business prepares items
3

Mark as Ready

When order is ready for pickup:
PATCH /api/empresa/pedidos/{id}/estado
{ "estado": "Listo" }
4

Customer Picks Up

Customer arrives at scheduled time (hora_recogida)
5

Mark as Delivered

After handing over order:
PATCH /api/empresa/pedidos/{id}/estado
{ "estado": "Entregado" }
Order is now complete and customer can leave a review.

Status Update Validation

PedidoController.php:213-217
if ($pedido->estado_pago !== 'aprobado') {
    return response()->json([
        'message' => 'No puedes modificar un pedido que no ha sido pagado.'
    ], 400);
}
Businesses cannot update order status unless payment is approved.

Customer Cancellation Flow

Customers can cancel orders under specific conditions:
1

Customer Requests Cancellation

POST /api/cliente/pedidos/{id}/cancelar
2

Validate Cancellation

PedidoController.php:175-177
if (strtolower($pedido->estado) !== 'pendiente') {
    return response()->json([
        'message' => 'No puedes cancelar un pedido ' . $pedido->estado
    ], 400);
}
Only orders in Pendiente state can be cancelled by customers.
3

Return Stock (if applicable)

PedidoController.php:182-186
if ($pedido->estado_pago === 'aprobado') {
    foreach ($pedido->productos as $producto) {
        $producto->increment('stock', $producto->pivot->cantidad);
    }
}
Stock is only returned if payment was already approved.
4

Update Order

PedidoController.php:188-191
$pedido->update([
    'estado' => 'Cancelado',
    'estado_pago' => 'rechazado'
]);

State Transition Rules

FromToTriggerActor
PendientePreparandoPayment approvedSystem (webhook)
PendienteCanceladoCancellation requestCustomer
PreparandoListoMarked readyBusiness
PreparandoCanceladoManual cancelBusiness
ListoEntregadoMarked deliveredBusiness
ListoCanceladoManual cancelBusiness

Stock Management

When Stock is Deducted

Payment Approval

Stock is deducted when payment is confirmed (webhook):
// In PagoController::webhook() after payment verification
foreach ($pedido->productos as $producto) {
    $producto->decrement('stock', $producto->pivot->cantidad);
}

$pedido->update([
    'estado' => 'Preparando',
    'estado_pago' => 'aprobado'
]);

When Stock is Returned

Cancellation

Stock is returned when paid orders are cancelled:
PedidoController.php:182-186
if ($pedido->estado_pago === 'aprobado') {
    foreach ($pedido->productos as $producto) {
        $producto->increment('stock', $producto->pivot->cantidad);
    }
}
If order was never paid, stock was never deducted, so nothing needs to be returned.

Order Model Structure

Pedido.php:12-19
protected $fillable = [
    'user_id',
    'empresa_id',
    'estado',       // 'pendiente', 'pagado', 'preparando', 'listo', 'entregado', 'cancelado'
    'estado_pago',  // 'pendiente', 'aprobado', 'rechazado'
    'hora_recogida',
    'total'
];

Relationships

Pedido.php:35-58
// Customer who placed the order
public function cliente()
{
    return $this->belongsTo(User::class, 'user_id');
}

// Business fulfilling the order
public function empresa()
{
    return $this->belongsTo(Empresa::class, 'empresa_id');
}

// Products in the order with pricing snapshot
public function productos()
{
    return $this->belongsToMany(Producto::class, 'pedido_productos')
                ->withPivot('cantidad', 'precio_unitario')
                ->withTimestamps();
}

Database Schema

pedidos
  ├── id (PK)
  ├── user_id (FK → users.id)
  ├── empresa_id (FK → empresas.id)
  ├── estado ('Pendiente', 'Preparando', 'Listo', 'Entregado', 'Cancelado')
  ├── estado_pago ('pendiente', 'aprobado', 'rechazado')
  ├── hora_recogida (time)
  ├── total (decimal)
  ├── created_at
  └── updated_at

pedido_productos (pivot)
  ├── id (PK)
  ├── pedido_id (FK → pedidos.id)
  ├── producto_id (FK → productos.id)
  ├── cantidad (int)
  ├── precio_unitario (decimal) -- snapshot of price at order time
  ├── created_at
  └── updated_at

Complete State Diagram

┌─────────────────────────────────────────────────────────────┐
│                     ORDER CREATED                           │
│                  (customer POST /pedidos)                   │
└────────────────────────┬────────────────────────────────────┘

                         v
              ┌──────────────────┐
              │   PENDIENTE      │ ◄── estado_pago: pendiente
              │                  │     Stock: NOT deducted
              └────┬─────────┬───┘     Visible to: Customer only
                   │         │
        Customer   │         │ Payment approved (webhook)
        cancels    │         │
                   │         v
                   │  ┌──────────────────┐
                   │  │   PREPARANDO     │ ◄── estado_pago: aprobado
                   │  │                  │     Stock: DEDUCTED
                   │  └────┬─────────┬───┘     Visible to: Business + Customer
                   │       │         │
                   │       │         │ Business updates
                   │       │         v
                   │       │  ┌──────────────────┐
                   │       │  │      LISTO       │
                   │       │  │                  │
                   │       │  └────┬─────────┬───┘
                   │       │       │         │
                   │       │       │         │ Business updates
                   │       │       │         v
                   │       │       │  ┌──────────────────┐
                   │       │       │  │   ENTREGADO      │ [TERMINAL]
                   │       │       │  │                  │
                   │       │       │  └──────────────────┘
                   │       │       │
                   │       │Business│
                   │       │cancels │
                   v       v       v
              ┌──────────────────┐
              │    CANCELADO     │ ◄── estado_pago: rechazado
              │                  │     Stock: RETURNED (if was deducted)
              └──────────────────┘     [TERMINAL]

Common Scenarios

  1. Customer places order → Pendiente / pendiente
  2. Customer pays via Mercado Pago
  3. Webhook confirms payment → Preparando / aprobado + stock deducted
  4. Business prepares order
  5. Business marks ready → Listo / aprobado
  6. Customer picks up order
  7. Business confirms → Entregado / aprobado
  1. Customer places order → Pendiente / pendiente
  2. Customer changes mind
  3. Customer cancels → Cancelado / rechazado
  4. No stock was deducted (no refund needed) ✅
  1. Customer places order → Pendiente / pendiente
  2. Payment approved → Preparando / aprobado + stock deducted
  3. Business realizes they can’t fulfill (e.g., item damaged)
  4. Business cancels → Cancelado / rechazado + stock returned
  5. Customer receives refund (handled by Mercado Pago) ✅
  1. Customer places order → Pendiente / pendiente
  2. Customer never pays
  3. Order remains Pendiente indefinitely
  4. No stock deducted, no impact on business
  5. Customer can cancel manually or create new order ⚠️
Consider implementing automatic expiration of unpaid orders after a timeout period.

API Endpoints Summary

Customer Endpoints

# Order Management
POST   /api/cliente/pedidos              # Create order
GET    /api/cliente/mis-pedidos          # View order history
POST   /api/cliente/pedidos/{id}/pagar   # Initiate payment
POST   /api/cliente/pedidos/{id}/cancelar # Cancel (if Pendiente)

Business Endpoints

# Order Fulfillment
GET   /api/empresa/pedidos                # View incoming orders
PATCH /api/empresa/pedidos/{id}/estado   # Update order status

System Endpoints

# Payment Processing
POST /api/webhook/mercadopago             # Receive payment notifications

Error Handling

Common errors and their solutions:

Insufficient Stock

{
  "message": "Stock insuficiente para: Café Latte. Disponible: 5"
}
Solution: Customer must reduce quantity or remove item

Cannot Cancel Non-Pending Order

{
  "message": "No puedes cancelar un pedido Preparando"
}
Solution: Customer must contact business directly

Cannot Update Unpaid Order

{
  "message": "No puedes modificar un pedido que no ha sido pagado."
}
Solution: Wait for payment confirmation before updating status

Duplicate Pending Order

{
  "message": "Ya tienes un pedido pendiente de pago.",
  "pedido": { ... }
}
Solution: Customer must pay or cancel existing order first

Best Practices

For Businesses

  • Check orders frequently during business hours
  • Update status promptly as orders progress
  • Communicate with customers if delays occur
  • Verify order details before marking as delivered

For Customers

  • Pay promptly after placing order
  • Arrive at scheduled pickup time
  • Cancel early if plans change
  • Leave reviews after receiving order

For Developers

  • Always validate payment status before allowing state changes
  • Use database transactions for stock operations
  • Log all state transitions for audit trail
  • Handle webhook failures gracefully with retries

For Admins

  • Monitor orders stuck in Pendiente state
  • Investigate payment failures
  • Handle customer disputes about cancelled orders
  • Review stock discrepancies

Next Steps

User Roles

Learn about permissions for each role

Business Workflow

Understand the business registration process

Build docs developers (and LLMs) love