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 replaces polling with persistent WebSocket connections for every time-sensitive interaction. Restaurants receive incoming orders the moment they are placed, customers watch their order status change in real time, and delivery drivers stream GPS coordinates directly to the customer’s map view — all without a single HTTP refresh. This page documents each channel’s URL pattern, message contract, and lifecycle.

Infrastructure

GastroMóvil runs under Daphne, an ASGI server that supports both HTTP and WebSocket protocols from a single process. Django Channels sits on top of Daphne and provides the consumer framework, while the channel layer is configured as an InMemoryChannelLayer — meaning no Redis or external broker is required.
# gastromovil/settings.py
ASGI_APPLICATION = 'gastromovil.asgi.application'

CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'channels.layers.InMemoryChannelLayer',
    },
}
InMemoryChannelLayer stores groups in process memory. If you run more than one Daphne worker process, each process has its own isolated layer and cross-process broadcasts will not work. For multi-process deployments, switch the backend to channels_redis.core.RedisChannelLayer.
The ASGI entry point combines HTTP and WebSocket routes via ProtocolTypeRouter, with AuthMiddlewareStack wrapping all WebSocket connections so that scope["user"] is available inside every consumer:
# gastromovil/asgi.py
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
import pedidos.routing
import repartidores.routing

application = ProtocolTypeRouter({
    'http': get_asgi_application(),
    'websocket': AuthMiddlewareStack(
        URLRouter(
            pedidos.routing.websocket_urlpatterns +
            repartidores.routing.websocket_urlpatterns
        )
    ),
})
The full list of registered WebSocket URL patterns (in match order) is:
PatternConsumerDefined in
ws/pedidos/<tipo>/<sala_id>/PedidoConsumerpedidos/routing.py
ws/repartidor/<repartidor_id>/RepartidorConsumerpedidos/routing.py
ws/ubicacion/<repartidor_id>/UbicacionConsumerrepartidores/routing.py
repartidores/routing.py also declares a ws/repartidor/<repartidor_id>/ pattern that maps to RepartidorConsumer, but because pedidos.routing.websocket_urlpatterns is prepended first in asgi.py, the pedidos/routing.py entry matches all ws/repartidor/ connections. The entry in repartidores/routing.py is never reached.

Channel 1 — Order Notifications (PedidoConsumer)

URL pattern: ws://<host>/ws/pedidos/<tipo>/<sala_id>/ PedidoConsumer is a general-purpose notification fan-out channel. Any backend signal can push a message to a named group, and every browser tab connected to that group receives it immediately. The group name is constructed by joining the two URL parameters: {tipo}_{sala_id}.

URL parameters

ParameterTypeDescription
tipostring (\w+)Category of the listener — typically restaurante or cliente
sala_idstring (\w+)ID of the entity — restaurant ID or client user ID
Examples:
  • ws://localhost:8000/ws/pedidos/restaurante/7/ — restaurant with ID 7 listens for new orders
  • ws://localhost:8000/ws/pedidos/cliente/42/ — customer with user ID 42 listens for status changes on their orders

Lifecycle

# pedidos/consumers.py (simplified)
async def connect(self):
    self.group_name = f"{self.tipo}_{self.sala_id}"
    await self.channel_layer.group_add(self.group_name, self.channel_name)
    await self.accept()

async def disconnect(self, close_code):
    await self.channel_layer.group_discard(self.group_name, self.channel_name)
On connect the consumer joins its group and accepts the handshake. On disconnect it removes itself from the group — no messages are lost for other connected members.

Events handled by PedidoConsumer

The consumer does not receive text frames from the browser; it is receive-only from the client’s perspective. It handles three server-side channel layer events:
Channel layer typeHandler methodForwarded to client?
notificacion_pedidonotificacion_pedido()✅ forwards event['data']
nuevo_pedidonuevo_pedido()✅ forwards entire event dict
pedido_disponiblepedido_disponible()✅ forwards entire event dict
notificacion_pedido extracts the data sub-key before sending, so the wire payload is the object inside data. The other two handlers serialize the whole channel layer event, which means the type key travels to the browser as well.

Channel 2 — Driver Location (UbicacionConsumer)

URL pattern: ws://<host>/ws/ubicacion/<repartidor_id>/ Both the delivery driver and the tracking customer connect to this same URL. The driver sends coordinate frames; the server persists each update to UbicacionRepartidor and fans it out to every member of the group (including back to the driver, and to any customer watching). Group name: ubicacion_{repartidor_id}

Sending a location update (driver → server)

{"latitud": 2.196, "longitud": -75.637}
The server writes the coordinates to the database with update_or_create (one row per driver) and then broadcasts to the group.

Receiving a location update (server → client)

{"tipo": "ubicacion", "latitud": 2.196, "longitud": -75.637}

Signalling delivery completion (driver → server)

{"tipo": "pedido_entregado"}
When the server receives this frame it skips the database write and broadcasts a delivery-complete notification to the group:
{"tipo": "pedido_entregado", "latitud": null, "longitud": null}
Clients receiving "tipo": "pedido_entregado" should hide the live map and show a delivery confirmation UI. The latitud and longitud values will be null for this event type and should be ignored.

Database persistence

Coordinates are written asynchronously using database_sync_to_async so the async consumer is never blocked by a synchronous ORM call:
# repartidores/consumers.py
@database_sync_to_async
def guardar_ubicacion(self, latitud, longitud):
    from usuarios.models import Usuario
    try:
        usuario = Usuario.objects.get(id=self.repartidor_id)
        UbicacionRepartidor.objects.update_or_create(
            repartidor=usuario,
            defaults={'latitud': latitud, 'longitud': longitud}
        )
    except Usuario.DoesNotExist:
        pass

Channel 3 — Driver Notifications (RepartidorConsumer)

URL pattern: ws://<host>/ws/repartidor/<repartidor_id>/ RepartidorConsumer is a private push channel for each delivery driver. On connect it joins two groups: its own private group repartidor_{repartidor_id} and, if the driver’s Repartidor.estado is 'disponible', the shared repartidores_disponibles broadcast group.
# repartidores/consumers.py (simplified)
async def connect(self):
    self.group_name = f'repartidor_{self.repartidor_id}'
    await self.channel_layer.group_add(self.group_name, self.channel_name)

    if await self.esta_disponible():
        await self.channel_layer.group_add("repartidores_disponibles", self.channel_name)
    await self.accept()

Events handled by RepartidorConsumer

Channel layer typeHandler methodNotes
notificacion_pedidonotificacion_pedido()Forwards event['data'] — used for direct assignment notifications
pedido_disponiblepedido_disponible()Forwards entire event — broadcast to all available drivers when a restaurant confirms an order
A driver who is not in 'disponible' state at connection time will not receive pedido_disponible broadcasts because it never joins repartidores_disponibles. The availability check runs once at connect time; reconnecting is required if the driver’s status changes.

JavaScript Client Examples

Connect a restaurant dashboard to receive live order alerts and a customer view to track their order status.
// Restaurant dashboard — listens for new orders
const restauranteId = 7;
const wsRestaurante = new WebSocket(
  `ws://${location.host}/ws/pedidos/restaurante/${restauranteId}/`
);

wsRestaurante.onmessage = (event) => {
  const data = JSON.parse(event.data);

  if (data.tipo === 'nuevo_pedido') {
    console.log(`New order #${data.pedido_id} from ${data.cliente}`);
    showOrderAlert(data);
  }

  if (data.tipo === 'notificacion_pedido') {
    console.log(data.mensaje);
  }
};

wsRestaurante.onclose = () => {
  console.warn('Restaurant WS closed — reconnecting...');
};

// Customer order tracking
const clienteId = 42;
const wsCliente = new WebSocket(
  `ws://${location.host}/ws/pedidos/cliente/${clienteId}/`
);

wsCliente.onmessage = (event) => {
  const data = JSON.parse(event.data);
  updateOrderStatusBadge(data.estado);
};

Event Reference

All payloads are JSON objects sent over the WebSocket text frame.

Events received by restaurant (restaurante_{id} group)

Sent when a customer places a new order. This message is delivered via the notificacion_pedido channel layer event, so the wire payload is the contents of event['data'] — it contains "tipo" (not "type").
{
  "tipo": "nuevo_pedido",
  "mensaje": "🍽️ ¡Nuevo pedido #101!",
  "pedido_id": 101,
  "cliente": "Ana García",
  "total": "25.50",
  "notas": "Sin cebolla, por favor"
}
FieldTypeDescription
tipostringEvent category — always "nuevo_pedido"
pedido_idintegerPrimary key of the new order
clientestringDisplay name of the customer
totalstringOrder total as a decimal string
notasstringCustomer delivery notes

Events received by customer (cliente_{id} group)

Sent whenever the order’s status is updated. The estado value is always one of the valid Pedido.ESTADOS codes: pendiente, aceptado, preparando, enviado, entregado, or cancelado.
{
  "tipo": "cambio_estado",
  "mensaje": "🛵 ¡Tu pedido está en camino!",
  "pedido_id": 101,
  "estado": "enviado"
}
FieldTypeDescription
pedido_idintegerOrder primary key
estadostringNew order status — one of pendiente, aceptado, preparando, enviado, entregado, cancelado
mensajestringHuman-readable description of the change

Events received by drivers (repartidor_{id} group)

Sent to a specific available driver’s private group (repartidor_{repartidor.usuario.id}) when the restaurant moves an order to enviado via the API endpoint. The pedido_disponible channel layer type is forwarded as the entire event, so type and tipo both appear in the wire payload.
{
  "type": "pedido_disponible",
  "tipo": "pedido_disponible",
  "pedido_id": 101,
  "restaurante": "Pizzería Roma",
  "direccion": "Carrera 8 #20-10, Neiva"
}
FieldTypeDescription
pedido_idintegerOrder to be claimed
restaurantestringOrigin restaurant name
direccionstringCustomer delivery address

Events on the location channel (ubicacion_{repartidor_id} group)

Event tipoDirectionlatitudlongitudNotes
ubicacionserver → clientdecimaldecimalBroadcast after every driver coordinate update
pedido_entregadoserver → clientnullnullDriver has confirmed delivery; close the map

Build docs developers (and LLMs) love