Skip to content

feat: customer notifications inbox API - #103

Merged
roncodes merged 4 commits into
release/v0.4.22from
feat/customer-notifications-inbox
Sep 28, 2026
Merged

roncodes merged 4 commits into
release/v0.4.22from
feat/customer-notifications-inbox

Conversation

@roncodes

@roncodes roncodes commented Sep 26, 2026 •

Copy link
Copy Markdown
Member

Summary

Adds a notifications inbox that storefront customers can read from the web and mobile app, built on core-api's notifications table. Storefront order notifications were already written there (the notifiable is the customer Contact), but no public endpoint exposed them.

Part of release #107 (release/v0.4.22). Stacked on #102 (push notification layer): until #102 merges into the release branch, this diff also shows #102's commits. Merge #102 first.

Endpoints (storefront/v1, authenticated with the Customer-Token header)

Method Path Description
GET notifications Newest first. Query: unread (bool), type (e.g. order_completed, promotional), limit (default 25, max 100), offset
GET notifications/unread-count { "count": n } for badges
GET notifications/{id} A single notification
PUT notifications/{id}/read Mark one as read
PUT notifications/read-all { "status": "OK", "updated": n }
DELETE notifications/{id} Delete one
GET / PUT notifications/preferences { "order_updates": bool, "promotions": bool }

Item shape:

{ "id": "…", "type": "order_completed", "title": "…", "body": "…", "image": null,
  "data": { "order_id": "order_…", "store_id": "store_…", "network_id": "network_…" },
  "is_read": false, "read_at": null, "created_at": "…" }

Behavior

  • Scoping: a customer only sees notifications addressed to them. Within that, the inbox is limited to the current storefront app: a store app sees rows for that store; a network app sees rows for the network and its member stores. Rows that reference no storefront appear in every app.
  • Old rows: order notifications written before fix: rebuild push notifications on a single storefront push channel #102 kept the status code in message and the title in subject. CustomerNotificationPresenter maps them to type/title/body, so existing history displays correctly.
  • Privacy: recipient details (email, phone, company), internal uuids and null values are stripped from data.
  • Preferences are stored on the contact's meta.notification_preferences:
    • order_updates: false turns off order status pushes only; the inbox copy is still written.
    • promotions: false suppresses promotional notifications entirely.
  • Realtime: order and promotional notifications broadcast the inbox item on contact.{uuid} / contact.{public_id} (through core's BroadcastNotificationCreated), with type equal to the inbox type. SafeBroadcastChannel catches socket failures, so a broadcast error can't fail the send and cause a retried listener to push twice.

Related Issue

Part of the notifications / promotions work. No tracking issue.

Type of Change

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

Implementation Notes

  • v1/NotificationController, Http/Resources/v1/CustomerNotification, Support/CustomerNotificationPresenter, Support/NotificationPreferences, Notifications/Channels/SafeBroadcastChannel.
  • Queries match notifiable_type in (FleetOps\Models\Contact, Storefront\Models\Customer), since either can be the notifiable. Storefront filtering uses JSON paths on data (store_id, network_id, storefront_id); a customer's rows are already narrowed by the existing notifiable morph index.
  • No core-api changes are needed.

Validation

  • Tests
  • Lint
  • Build (no frontend changes in this PR)
  • Manual validation
composer test:unit -> 491 tests, 3140 assertions, 0 failures
composer test:lint -> Found 0 of 267 files that can be fixed

New: server/tests/Unit/Http/Controllers/CustomerNotificationInboxTest.php. It covers auth, store vs network scoping, filters and pagination, cross-customer isolation, read/read-all/delete, legacy rows, preferences and their effect on delivery, the realtime payload, and broadcast failure isolation.

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: all endpoints in the table above are new (Storefront → Notifications).

Documentation Notes

fleetbase.io, Storefront API → Notifications: the endpoints, item shape, preferences, and the realtime channel (contact.{uuid}) for app developers.

Risk

  • Additive: new routes, plus a broadcast delivery after push, database and mail. Broadcast failures are logged, not thrown.
  • Customers who turn promotions off no longer receive promotional pushes. The existing admin send endpoint still counts them in sent_count; that endpoint is replaced by campaigns in the promotions work.
  • The mobile/web app inbox screen and socket subscription are separate follow-ups in fleetbase/storefront-app.

Android pushes never worked with store-level FCM credentials and some iOS
devices were rejected, because of several stacked defects:

- configureFcm put the service account JSON into credentials.private_key,
  so no valid Firebase client was ever built from a store channel
- FcmChannel required the platform-wide Firebase project to be configured
- credentials were always resolved from the store, never the network app
- one APNs environment per channel rejected sandbox/dev build tokens
- registerDevice never reassigned a token to the latest customer and did
  not normalize platform casing
- mail/database errors ran before push and stopped it; failures and dead
  tokens were never handled or logged

Introduce Push\StorefrontPushChannel with isolated Firebase/APNs clients,
network-first credential resolution with wrong-app and wrong-environment
retries, dead token pruning, and high priority payloads. Order
notifications share a StorefrontOrderNotification base class. Add a
customers/unregister-device endpoint and an admin test push action.
@roncodes roncodes added needs-docs Requires documentation updates needs-api-spec Requires API specification updates type:feature Feature or enhancement labels Sep 26, 2026
Codecov requires full patch coverage. Cover the FCM and APNs transport
send paths, PushMessage setters, APNs environment short-circuit, explicit
push routes, and pruning/logging failure handling. Read device
attributes with data_get so devices returned by a custom push route do
not need to be Eloquent models, and drop two unreachable branches.
Storefront order notifications were already stored in the core
notifications table, but customers had no way to read them. Add
storefront/v1/notifications endpoints for the authenticated customer to
list (filter by unread/type, limit/offset), count unread, read, mark as
read, mark all as read and delete their notifications, scoped to the
storefront app (store, or network and its member stores).

Add customer notification preferences (order update pushes, promotions)
stored on the contact meta and honored by the notifications, and
broadcast new notifications in realtime on contact.{uuid} through a
broadcast channel that never fails the send.
@roncodes
roncodes force-pushed the feat/customer-notifications-inbox branch from 5dfcf46 to 467a420 Compare September 26, 2026 08:14
…asts

Also drop null values from inbox item data, and read notification data
through the model's array cast.
@roncodes
roncodes changed the base branch from fix/push-notification-layer to release/v0.4.22 September 28, 2026 03:21
@roncodes
roncodes merged commit a29db54 into release/v0.4.22 Sep 28, 2026
@roncodes
roncodes deleted the feat/customer-notifications-inbox branch September 28, 2026 04:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-api-spec Requires API specification updates needs-docs Requires documentation updates type:feature Feature or enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant