Skip to content

feat(chat): customer chat with the driver delivering their order - #117

Merged
roncodes merged 4 commits into
release/v0.4.25from
feat/order-chat
Oct 7, 2026
Merged

roncodes merged 4 commits into
release/v0.4.25from
feat/order-chat

Conversation

@roncodes

@roncodes roncodes commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

Summary

The storefront-app redesign lets a customer message the driver delivering their order, from the tracking screen and from push notifications. Fleetbase core already has chat channels, and the driver app already lists them. But customers couldn't use them:

  • core's chat API needs a Fleetbase API key and user, not a storefront key with a Customer-Token;
  • nothing linked a chat to an order;
  • customers' devices are registered with the storefront, so core's chat notifications never reached them.

This PR adds an order chat for storefront customers. It is built on core chat channels, so the driver side works unchanged.

Endpoint (storefront key + Customer-Token) What it does
GET storefront/v1/orders/{id}/chat Returns the chat, starting it if needed: {id, channel, order, status, me, participants[], messages[] (latest 50, oldest first), unread_count}
GET storefront/v1/orders/{id}/chat/messages?before={message_id}&limit= Older messages. Up to 100, oldest first
POST storefront/v1/orders/{id}/chat/messages Sends {content?, files?: [{data (base64), type: image/*}]}, at most 4 photos
POST storefront/v1/orders/{id}/chat/read Marks every driver message as read. Returns {read: n}

This branch is stacked on #113 (socket auth). The customer socket token gains the customer's user uuid in ids, so the app can subscribe to chat_channel.{uuid}. Core (fleetbase/core-api#290) authorizes those channels by participant user.

Related Issue

Part of the storefront-app redesign (driver chat on order tracking).

Type of Change

  • Bug fix
  • Feature
  • Refactor
  • Documentation
  • Test
  • Chore

Implementation Notes

  • Support/OrderChat
    • The order's chat is a Fleetbase\Models\ChatChannel with meta.storefront_order_uuid and meta.storefront_order_id, named "Order {public_id}".
    • open() creates it with the customer's user as creator, which core adds as the first participant. It then syncs participants to exactly the customer and the currently assigned driver's user. On reassignment, the previous driver is removed and the new one added.
    • It returns null when the customer or driver has no user account; in practice, when no driver is assigned yet.
  • OrderChatController
    • Requires a customer token (401) and a storefront order (404). The order's storefront_id must match a store key, or its storefront_network_id a network key. The order must belong to the customer (403).
    • When there is no driver: 409 {reason: driver_not_assigned}.
    • Once the order is completed, canceled or expired, the chat is read-only: status: closed, and sending returns 423 {reason: chat_closed}. It also isn't started for an order that finished without one.
    • Pagination uses a (created_at, id) cursor, so messages sent in the same second aren't skipped.
    • Photos are written without a public ACL, like review photos in fix(reviews): upload review photos without a public ACL #112, and recorded as File (type = storefront_chat_upload, with disk) plus ChatAttachment.
    • After sending, core's notifyParticipants() notifies the driver (broadcast, FCM/APNs) as it does for any chat.
  • Resources OrderChat and OrderChatMessage give the app a stable shape. Each participant and sender carries role: customer|driver, and each message has is_mine and read. avatar_url is null when the user has no avatar, so the app can show initials.
  • ChatMessageObserver sends StorefrontOrderChatMessage when someone other than the customer writes in an order chat. That goes through storefront push, the database inbox and the customer broadcast channel. The push reads "Ravi K. sent a message" or "Sent a photo", with type, order_id, chat_id and message_id. It isn't gated by notification preferences, because it is about an active order.
  • HandleOrderDriverAssigned starts the chat when a driver is assigned, so it is already in the driver's Navigator chat list. A failure is reported and doesn't affect the existing driver-assigned notification.
  • StorefrontSocket::customerPrincipal now puts user_uuid in ids when the customer has one.

Validation

  • Tests
  • Lint
  • Build
  • Manual validation

Command output / summary:

Full backend suite (local, PHP 8.4, Pest), with core-api#290's SocketCluster classes
added to the local vendor copy so #113's socket tests can run:
OK (612 tests, 3686 assertions)

Without them, only #113's existing socket and checkout socket tests error
(SocketToken / SocketPrincipal / SocketChannelRegistry not found), same as #113 itself.

Line coverage of every changed or added file in server/src (Xdebug): all covered
  Support/OrderChat, Http/Controllers/v1/OrderChatController,
  Http/Resources/OrderChat + OrderChatMessage, Http/Requests/SendOrderChatMessageRequest,
  Notifications/StorefrontOrderChatMessage, Observers/ChatMessageObserver,
  Listeners/HandleOrderDriverAssigned, Support/StorefrontSocket

php-cs-fixer: clean for all changed files

New server/tests/Unit/Http/Controllers/OrderChatControllerTest.php (17 tests) covers:

  • auth and context (signed out, unknown order, another store, another customer, unknown key);
  • no driver yet, and a finished order without a chat;
  • one channel per order, with participants and roles;
  • network orders;
  • sending text and a photo (private write, File and ChatAttachment records, the driver notified through core);
  • driver messages pushed to the customer, but the customer's own messages not;
  • cursor pagination with messages in the same second;
  • unread counts and read receipts;
  • driver reassignment;
  • the closed state;
  • a customer missing from the channel;
  • every observer early return;
  • the notification payloads;
  • request rules;
  • the driver-assigned listener, including a failure starting the chat.

StorefrontSocketTest now asserts the user uuid in customer ids, and that a customer without a user keeps contact ids only. The route contract test lists the four chat routes.

Documentation Impact

  • No documentation changes needed
  • Documentation updated in fleetbase/fleetbase.io
  • Documentation needed but not included

API Reference Impact

  • No API reference changes needed
  • Updated fleetbase/postman
  • API reference updates required but not included

API reference notes:

  • The four endpoints above.
  • The new push and inbox notification type order_chat_message.
  • Customer socket tokens' ids now include the user uuid.

Documentation Notes

The storefront API docs and Postman need the order chat endpoints and the socket channel name (chat_channel.{uuid}, returned as channel).

Risk

Socket channel

The chat payload's channel is now chat.{public_id}. Core broadcasts chat lifecycle events (chat_message.created and so on) on chat.{id}, and core-api#290 authorizes that name for participants. The earlier chat_channel.{uuid} was authorized but never broadcast on.

…annel resolvers

- POST storefront/v1/customers/socket-token mints a customer principal (store key +
  Customer-Token); 404 while socket auth is disabled, 401 without a customer of the
  storefront's company.
- Checkout initialization responses carry socket_token (checkout kind, scp limited to
  checkout.{public_id}) when socket auth is enabled; absent otherwise.
- Register storefront and checkout channel resolvers with core-api's
  SocketChannelRegistry.
- QPay capture publishes {checkout, status, order, error} on checkout.{public_id}
  instead of the raw payment row; a publish failure no longer fails the callback.
- Require fleetbase/core-api ^1.6.69.
- storefront/v1/orders/{id}/chat: show (starts the chat), messages (cursor
  pagination), send (text and up to four photos) and read (receipts), for the
  signed-in customer's own orders in this storefront.
- The chat is a core chat channel tagged with the order in its meta, so the
  driver sees it in Navigator; participants are kept to the customer and the
  currently assigned driver, and it is started when a driver is assigned.
- Customers can read but not send once the order is completed, canceled or
  expired.
- Driver messages are pushed to the customer through storefront push, the inbox
  and the customer's broadcast channel.
- Customer socket tokens include the customer's user uuid so they can subscribe
  to chat_channel.{uuid}, which core authorizes by participant.
@roncodes
roncodes changed the base branch from feature/socket-auth to release/v0.4.25 October 6, 2026 19:14
…-chat

# Conflicts:
#	server/tests/Unit/Routes/StorefrontRoutesTest.php
@roncodes
roncodes merged commit 7d71b2a into release/v0.4.25 Oct 7, 2026
@roncodes
roncodes deleted the feat/order-chat branch October 7, 2026 05:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant