Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/lffiesco-svg/gastromovil/llms.txt

Use this file to discover all available pages before exploring further.

GastroMóvil’s ordering flow is built on a lightweight session-based cart that requires no database writes until the customer confirms their purchase. When the order is placed, the system geocodes the delivery address, persists every order line, notifies the restaurant over WebSocket, and then tracks the order through a strict state machine until the driver marks it delivered.

The Session Cart

The Carrito class in carrito/cart.py stores everything in request.session under the key 'carrito'. Each entry in the session dictionary is keyed by the product’s primary key (as a string) and holds all the data needed to render the cart without additional database queries.
CART_SESSION_KEY = 'carrito'

class Carrito:
    def __init__(self, request):
        self.session = request.session
        self.carrito = self.session.get(CART_SESSION_KEY, {})

Cart Item Structure

When agregar() is called, a new entry is created (or incremented) with this shape:
self.carrito[pid] = {
    'nombre':             producto.nombre,
    'precio':             str(producto.precio),
    'cantidad':           0,               # incremented each call
    'imagen_url':         producto.imagen.url if producto.imagen else None,
    'restaurante_id':     producto.categoria.restaurante.id,
    'restaurante_nombre': producto.categoria.restaurante.nombre,
}

Available Methods

MethodSignatureDescription
agregaragregar(producto, cantidad=1)Add one or more units of a product
limpiarlimpiar()Clear the entire cart
totaltotal()Return the subtotal (sum of precio × cantidad for all items) as a float
itemsitems()Return an iterable of all cart item dictionaries
restaurante_idrestaurante_id()Return the restaurante_id of the first item in the cart
The cart does not prevent a customer from adding products from different restaurants in the same session. At checkout, the confirmar_pedido view groups items by restaurante_id and creates one Pedido per restaurant automatically.

Cart Endpoints

GET  /carrito/                        — render the cart page
POST /carrito/agregar/<producto_id>/  — add a product (login required)
POST /carrito/restar/<producto_id>/   — decrement quantity
POST /carrito/eliminar/<producto_id>/ — remove product from cart
POST /carrito/vaciar/                 — empty the cart
POST /carrito/confirmar/              — place the order

Order Placement (POST /carrito/confirmar/)

The confirmation endpoint accepts a JSON body and performs several validation steps before persisting anything.

Required Payload

{
  "direccion":    "Calle 5 # 10-20",
  "barrio":       "centro",
  "notas":        "Timbre roto, llamar al llegar",
  "metodo_pago":  "nequi"
}

Supported Payment Methods

metodos_validos = ['efectivo', 'nequi', 'daviplata']

Barrio Validation

The barrio field must exactly match one of the ~50 recognised neighbourhoods of Garzón, Huila that are hardcoded in BARRIOS_GARZON:
BARRIOS_GARZON = {
    'centro', 'bello horizonte', 'villa del rio', 'la paz', 'el jardin',
    'san jose', 'la esperanza', 'el recreo', 'las americas', 'el bosque',
    'villa cafe', 'santa barbara', 'el porvenir', 'los angeles', 'la union',
    # … ~50 total
}
An order with an unrecognised barrio value is rejected immediately with "El barrio ingresado no es válido. Verifica el nombre de tu barrio en Garzón." — no partial save occurs.

Address Validation

Before geocoding, the view enforces three address rules:
  1. direccion must be at least 5 characters.
  2. It must contain at least one digit (e.g. a street number).
  3. It must contain at least one letter.

Address Geocoding

After validation passes, the view calls the Google Maps Geocoding API to resolve the full address into latitude/longitude coordinates. The resulting lat/lng values are saved to the Direccion model:
query = urllib.parse.quote(f"{direccion}, {barrio}, Garzón, Huila, Colombia")
url   = f"https://maps.googleapis.com/maps/api/geocode/json?address={query}&key={GOOGLE_API_KEY}"

Delivery Fee

A flat delivery fee of 3,000 COP is added to every order total:
DOMICILIO = 3000
pedido = Pedido.objects.create(
    ...
    total=total_restaurante + DOMICILIO,
)

The Pedido and DetallePedido Models

class Pedido(models.Model):
    ESTADOS = [
        ('pendiente',  'Pendiente'),
        ('aceptado',   'Aceptado'),
        ('preparando', 'Preparando'),
        ('enviado',    'Enviado'),
        ('entregado',  'Entregado'),
        ('cancelado',  'Cancelado'),
    ]
    cliente           = models.ForeignKey(Usuario, ...)
    restaurante       = models.ForeignKey(Restaurante, ...)
    direccion_entrega = models.ForeignKey(Direccion, on_delete=models.SET_NULL, ...)
    estado            = models.CharField(choices=ESTADOS, default='pendiente')
    fecha             = models.DateField(auto_now_add=True)
    total             = models.DecimalField(max_digits=10, decimal_places=2)
    notas             = models.TextField(blank=True)
    repartidor        = models.ForeignKey(Usuario, null=True, related_name='entregas')

class DetallePedido(models.Model):
    pedido          = models.ForeignKey(Pedido, related_name='detalles')
    producto        = models.ForeignKey(Producto, ...)
    cantidad        = models.IntegerField()
    precio_unitario = models.DecimalField(max_digits=10, decimal_places=2)

    @property
    def subtotal(self):
        return int(self.cantidad * self.precio_unitario)

Order State Machine

An order moves through the following lifecycle. Only the restaurant can advance states from pendiente through enviado; a customer can cancel from pendiente or aceptado.
                ┌──────────────────────────────────────────────────────┐
                │                                                      │
  [placed]      ▼                                                      │
  pendiente ──► aceptado ──► preparando ──► enviado ──► entregado     │
      │                                                                │
      └──────────────────────► cancelado ◄──────────────────────────┘
                              (from pendiente or aceptado only)

WebSocket Notifications

GastroMóvil uses Django Channels to push real-time notifications at two points in the order lifecycle.
1

New Order → Notify Restaurant

When confirmar_pedido persists the order, it immediately publishes a notificacion_pedido event to the restaurante_{id} channel group:
channel_layer = get_channel_layer()
async_to_sync(channel_layer.group_send)(
    f'restaurante_{restaurante.id}',
    {
        'type': 'notificacion_pedido',
        'data': {
            'tipo':     'nuevo_pedido',
            'mensaje':  f'🍽️ ¡Nuevo pedido #{pedido.id}!',
            'pedido_id': pedido.id,
            'cliente':  request.user.get_full_name() or request.user.username,
            'total':    str(pedido.total),
        }
    }
)
2

State Change → Notify Customer

Every time the restaurant changes an order’s state via the panel API, a cambio_estado event is published to the cliente_{id} group with a user-friendly message:
mensajes_estado = {
    'aceptado':   '✅ ¡Tu pedido fue aceptado! El restaurante lo está preparando.',
    'preparando': '👨‍🍳 Tu pedido está siendo preparado.',
    'enviado':    '🛵 ¡Tu pedido está en camino!',
    'entregado':  '🎉 ¡Tu pedido fue entregado!',
    'cancelado':  '❌ Tu pedido fue cancelado.',
}
3

Aceptado → Notify Available Drivers

When an order reaches the 'aceptado' state, a post_save signal on Pedido broadcasts a pedido_disponible event to the shared repartidores_disponibles channel group so every connected driver with estado='disponible' receives it:
@receiver(post_save, sender=Pedido)
def notificar_cambio_pedido(sender, instance, created, **kwargs):
    ...
    elif instance.estado == 'aceptado':
        async_to_sync(channel_layer.group_send)(
            "repartidores_disponibles",
            {
                "type":        "pedido_disponible",
                "pedido_id":   instance.id,
                "restaurante": instance.restaurante.nombre,
                "direccion":   str(instance.direccion_entrega),
            }
        )
The PedidoConsumer (pedidos/consumers.py) implements the WebSocket handler. Clients connect using a generic group pattern:
ws://<host>/ws/pedidos/<tipo>/<sala_id>/
Where tipo is either restaurante or cliente and sala_id is the corresponding entity’s primary key.

Order History and Detail

URLDescription
/mispedidos/Lists all orders placed by the logged-in customer, ordered by most recent date.
/pedidos/<pk>/Shows a single order with its full DetallePedido breakdown and current status.
Customers can cancel orders that are still in pendiente or aceptado state. Once preparation begins (preparando), cancellation is no longer available.

Build docs developers (and LLMs) love