POS Gateway - Consumers
Location: api/nextango/apps/pos_gateway/consumers.py
Related: api/nextango/apps/pos_gateway/channels_middleware.py, api/nextango/apps/pos_gateway/routing.py
Last Updated: 2026-07-09
Overview
Two Django Channels AsyncWebsocketConsumer classes provide real-time, bidirectional communication for POS operations: TerminalConsumer for individual terminals and ManagerConsumer for manager dashboards. Both use Redis-backed channel groups for broadcasting; a group message's type field maps directly to a consumer method name (Channels' standard dispatch convention).
Three channel group patterns are in use: terminal_{terminal_id} (a single terminal), store_{store_id} (all terminals in a store), and managers_{store_id} (all manager dashboards for a store).
WebSocket Authentication
WebSocket connections authenticate through PosGatewaySessionMiddleware (channels_middleware.py), not through the consumer's connect() method directly. Because a browser cannot set a custom Authorization header on a WebSocket upgrade, the pos_session JWT is passed as a ?token= query parameter on the connection URL instead.
The middleware validates the token via PosGatewayAuthenticationClient.validate_session_token() and populates scope['user'] and scope['pos_session'] before the consumer runs. It never rejects the handshake itself. On any failure (missing token, invalid JWT, unresolvable user) it leaves scope['user'] as AnonymousUser and scope['pos_session'] as None, and the consumer's own connect() method decides whether to accept or close. In DEBUG, a dev-token-* token is recognized the same way DevTokenAuthentication recognizes it on the HTTP side (see Authentication).
TerminalConsumer.authenticate_terminal() reads scope['pos_session']. For a real session (not the dev token), it also requires the JWT's terminal_id to match the URL's terminal_id, and refreshes the corresponding TerminalSession row (status, expires_at, last_heartbeat) so the database state tracks a still-valid JWT even if the row had separately expired.
TerminalConsumer
WebSocket URL: /ws/pos-gateway/terminal/<terminal_id>/
On connect(), authenticates via the flow above, joins the terminal_{terminal_id} and store_{store_id} groups, accepts the connection, and sends a connection_confirmed message. Closes with code 4001 on authentication failure or 4002 on any other connection error. disconnect() removes the connection from both groups.
Inbound message types (receive())
heartbeat: updatesTerminalSession.last_heartbeatand replies withheartbeat_response.manager_approval_request: creates aManagerApprovalRequestviaManagerApprovalGatewayService.request_approval(), then broadcasts amanager_approval_neededevent to themanagers_{store_id}group and replies to the terminal withapproval_request_created. This is the actual notification path for a new approval request;ManagerApprovalGatewayService._broadcast_approval_request()is a no-op placeholder and is not what notifies managers.inventory_check: always replies withavailable_quantity: 0, reserved_quantity: 0. Inventory lookup via the POS gateway was removed; the POS frontend calls the products API directly for barcode and SKU lookups. This handler exists for backward compatibility with older terminal clients and returns a safe, static default rather than a real inventory count.
Outbound event handlers (group messages forwarded to the client)
inventory_update, approval_decision, system_notification, transaction_created, transaction_updated, transaction_completed, return_processed, each formats the incoming group message as JSON and sends it to the connected client. transaction_completed is triggered by WebSocketBroadcaster.broadcast_transaction_completed() (see Broadcast Utilities) after a payment completes.
ManagerConsumer
WebSocket URL: /ws/pos-gateway/manager/<store_id>/
On connect(), calls authenticate_manager(), joins the managers_{store_id} group, and accepts. authenticate_manager() is currently a stub; it returns a hardcoded success without validating a real manager identity or role. Do not treat this connection as authenticated in the same sense as TerminalConsumer; the same PosGatewaySessionMiddleware runs ahead of it, but the consumer's own connect() does not yet check scope['pos_session'] or the user's role.
Inbound message types (receive())
approval_decision: updates theManagerApprovalRequeststatus (approvedordenied), then sends anapproval_decisionevent to theterminal_{terminal_id}group for the requesting terminal.store_notification: broadcasts asystem_notificationevent to thestore_{store_id}group, reaching every terminal in the store.
Outbound event handlers
manager_approval_needed: forwards a new approval request notification (raised by TerminalConsumer.handle_manager_approval_request) to the manager dashboard as an approval_request message.
Complete Message Flow (Manager Approval)
A terminal sends manager_approval_request. TerminalConsumer creates the ManagerApprovalRequest row and broadcasts manager_approval_needed to managers_{store_id}. Every connected ManagerConsumer in that group receives it and forwards approval_request to its dashboard. A manager responds with approval_decision. ManagerConsumer updates the database row and sends approval_decision to terminal_{terminal_id}. The originating TerminalConsumer forwards it to its client.
Related Documentation
- Models: TerminalSession, ManagerApprovalRequest
- Authentication: JWT-based terminal session authentication,
PosGatewaySessionMiddleware - Services:
ManagerApprovalGatewayService - Views: REST endpoints for the same operations
- Broadcast Utilities: server-initiated broadcasts to
TerminalConsumer