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: updates TerminalSession.last_heartbeat and replies with heartbeat_response.
  • manager_approval_request: creates a ManagerApprovalRequest via ManagerApprovalGatewayService.request_approval(), then broadcasts a manager_approval_needed event to the managers_{store_id} group and replies to the terminal with approval_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 with available_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 the ManagerApprovalRequest status (approved or denied), then sends an approval_decision event to the terminal_{terminal_id} group for the requesting terminal.
  • store_notification: broadcasts a system_notification event to the store_{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.


  • 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

Was this page helpful?