-
In-app notifications — bell widget + notifications page, driven by a live feed from the notification microservice
migration-service
notification
api_gateway
ui_gateway
admin_ui
admin_backend
profile_microservice
account_microservice
finance_microservice
helpcenter
Tenant DB migration required. Run php artisan tenant:migrate {slug} for every active tenant before this release goes live. Adds read_at column + composite index on the notifications table. Without it the user feed endpoints fail with SQL errors. Template seeder required. Run php artisan tenant:seed {slug} --class=InAppTemplateSeeder for every tenant. Seeds the single generic_in_app template. Without it, the in-app channel silent-skips every send (nothing appears in the bell). Env vars required on newly-scaffolded services. account_microservice and helpcenter now talk to the notification microservice. Set NOTIFICATION_SERVICE_URL, NOTIFICATION_SERVICE_TIMEOUT, NOTIFICATION_SERVICE_ENABLED, and INTERNAL_API_KEY on both servers. INTERNAL_API_KEY must match the value on the notification microservice. Deployment order: migration-service → notification → api_gateway → ui_gateway + admin_ui → admin_backend + profile_microservice + account_microservice + finance_microservice + helpcenter. Cache clears: after deploy, run php artisan config:clear && php artisan route:clear on every service touched, plus php artisan view:clear on ui_gateway + admin_ui.
- Single shared `generic_in_app` template renders every in-app notification; per-event copy (title, body, deep-link) is supplied by the call site. Collapses template authoring from 64 slugs to 1.
- `NotificationDispatcher::send()` refactored to accept `channels: ['in_app', 'email']` and fan out per channel. A channel with no active template is skipped silently so the other channel still fires — this keeps email and in-app tracks independent.
- Back-compat shim: legacy single-channel `channel: 'email'` payloads still work (the live `bank_account_deleted` integration and future RBS reactive actions keep running without coordinated redeploy).
- New migration `2026_04_23_120001_add_read_at_to_notifications` — adds `read_at` nullable timestamp + composite index `(recipient_id, channel, read_at)` for the three hot feed queries (unread count, list, mark-all-read)
- `InAppTemplateSeeder` seeds `generic_in_app` with `title`/`body`/`link` variables; registered in `DatabaseSeeder` so every new tenant gets it automatically
- `InAppChannel` added to the notification microservice (no external delivery — the row's presence in the tenant DB IS the delivery; queue worker marks `status=sent` for admin-log consistency)
- `RejectReadonlyToken` middleware + `readonly.token` alias — blocks admin impersonation tokens from POSTing to mark-read endpoints so the user's true unread state is preserved
- Four new user-facing endpoints on the notification microservice under `auth:sanctum` + `readonly.token`: `GET /user/notifications`, `GET /user/notifications/unread-count`, `POST /user/notifications/{id}/read`, `POST /user/notifications/read-all`
- Matching proxy endpoints on api_gateway under `/api/{tenant}/user/notifications/*` — forwards the user's bearer token + X-Tenant header (no X-Internal-Key — authenticates as the real Sanctum user on the downstream service)
- Desktop header gains a live bell dropdown with unread-count badge and a "Mark all as read" button; mobile avatar menu gets a badged bell link
- Polling runs every 60s via `public/assets/custom_js/notifications-bell.js`, pauses when the tab is hidden (`document.visibilityState`) and resumes on visibility change — zero network chatter for inactive tabs
- `/notifications` page rewritten as a paginated list with All / Unread filters and per-row mark-as-read, driven by `public/assets/custom_js/notifications-page.js`
- admin_ui template editor adds `In-App` to the channel dropdown; admin notification log gains an `In-App` channel filter and renders `in_app` as "In-App" rather than "In_app"
- `admin_backend` (40 events): KYC approved/rejected, document status changed, document remarks, admin uploads (identity/address/networth), admin-verify-email, profile-updated-by-admin, withdrawal approve/reject/comment, deposit approve/reject/comment, internal transfer approve/reject/comment, admin password reset, ticket replied, ticket created by admin, account block/unblock/deactivate, wallet credit/debit (Clients + Accounts views), internal transfer by admin, admin-opened live/demo accounts, MT5 leverage update, MT5 password change by admin, MT5 manual deposit/withdrawal, MT5 account delete/transfer/disable, MT5 account request approve/reject
- `profile_microservice` (12 events): password changed, profile updated (self), document uploads (identity, address, networth, signature), bank account added/updated/removed, crypto address added/updated/removed
- `finance_microservice` (8 events): withdrawal OTP, withdrawal submitted (bank + crypto), deposit submitted, deposit receipt uploaded, deposit received (PSP webhook), deposit expired (auto-cancel after 4h), internal transfer submitted
- `account_microservice` (3 events, new scaffold): live account created (self-service), demo account created (self-service), MT5 password changed (self)
- `helpcenter` (1 event, new scaffold): support ticket created
- ~15 user-facing events skipped as Phase 3 — mostly auth signup/password-reset flows where the user isn't logged in yet, promotion broadcasts (deferred to P4 in the email track), and admin-initiated soft-delete (a deleted user cannot see their bell). Full rationale in `documents/IN_APP_NOTIFICATIONS_PLAN.md` §9.
- RBS `notify_user_inapp` reactive action — deferred until the RBS microservice goes live. Adding it later is a ~15-line follow-up (one entry in `rbs/config/catalog.php`, one case in `api_gateway/app/Services/RbsActionExecutor.php`).
- Admin-side in-app bell — tracked as Milestone E. The backend infrastructure supports it (schema has `recipient_type='admin'`), but the admin-side UI + endpoints are a separate project.
- Every call site wraps the notification dispatch in try/catch; notification-service failures never block the business action they accompany (KYC decision, withdrawal approval, deposit credit, etc.)
- Existing direct `Mail::to()` email paths are untouched — in-app fires alongside email via `channels: ['in_app']`. When each per-event email template is authored on migration-service, its call site will switch to `channels: ['in_app', 'email']` and retire the direct-mail block in the same PR. This avoids double-sending emails during the transition.
- Readonly (admin impersonation) tokens can list / count notifications but cannot mark anything read — preserves the real user's unread state when an admin is viewing their account.
-
NotificationSettingSeeder for encrypted SendGrid configuration
migration-service
REQUIRED on every tenant before this release goes live. Must be run against every active tenant DB (dev, demo, zyz, okm, and any new tenants added later). Without this, the notification microservice has no SendGrid key to read and every email send will fail, breaking all flows that depend on email.
- Seeds `sendgrid_api_key`, `from_email`, `from_name` rows in the tenant `notification_settings` table
- Values read from `SEED_SENDGRID_API_KEY`, `SEED_FROM_EMAIL`, `SEED_FROM_NAME` env vars (not committed to git)
- `sendgrid_api_key` is encrypted at rest with Laravel `Crypt` (APP_KEY, shared across internal microservices)
- Safe to re-run: skips any key whose row already has a non-empty value, and skips any key whose seed env var is empty
- Registered in `DatabaseSeeder`; run per-tenant via `php artisan tenant:seed {slug} --class=NotificationSettingSeeder`
-
Encrypted-at-rest handling for
sendgrid_api_key in NotificationSetting model
notification
- `NotificationSetting::get()` / `::set()` transparently decrypt/encrypt values for keys listed in `$encryptedKeys`
- `EmailChannel` unchanged: continues to call `NotificationSetting::get('sendgrid_api_key')` and receives plaintext
- Decryption failures log a warning and return the default instead of crashing the send path
-
Template versioning for the notification microservice
migration-service
notification
REQUIRED to run tenant migrations on every tenant before this release goes live. Five new migrations (timestamps 2026_04_20_120001-120005) add the notification_template_versions table, current_version_id on templates, template_version_id + raw_body on notifications, backfill existing data, and drop the legacy body_preview column. Run for every tenant (dev, demo, zyz, okm, and any future tenants): php artisan tenant:migrate {slug}. Until these run, the notification microservice will reject new sends because it expects current_version_id to be set on every active template. Deployment order: deploy migration-service → run migrations → deploy the notification microservice → restart queue workers. Deploying the new notification code before the migrations run will cause inserts to fail because the new columns don't exist yet.
- Every notification now pins a specific `template_version_id` at dispatch time, so past emails can be re-rendered exactly as delivered even if the template is edited later
- Template edits create a new immutable version row with SHA-256 content-hash dedup (identical re-saves don't spam new versions)
- Rendered email body is no longer persisted for template-based sends — re-rendered from the pinned version at send time by `ProcessNotification`, cutting notifications-table storage from a few KB/row to a few hundred bytes/row
- Subject is still rendered and stored at dispatch time for fast admin log display
- Raw sends (`/api/send-raw`) continue to store the full body inline in a new `raw_body` column since there is no template to re-render from. See `documents/TECH_DEBT.md` for the planned retirement of this path
- Admin CRUD API contract unchanged — no admin_ui or api_gateway code changes required for this release
- Channel handlers now receive a `RenderedMessage` DTO instead of reading a body column from the notification model; `EmailChannel` updated accordingly, any future channel must follow the same contract
- `NotificationTemplate::saveVersion()` helper called on every template create/update by `AdminTemplateController`
-
First notification microservice integration —
bank_account_deleted email
migration-service
profile_microservice
Requires the template seed to be re-run on every tenant. Run php artisan tenant:seed {slug} --class=NotificationTemplateSeeder for dev, demo, zyz, okm. The existing four templates are deduped via SHA-256 hash; only the new bank_account_deleted row is inserted.
- New `bank_account_deleted` template in `NotificationTemplateSeeder` (migration-service). Modern Metronic-style HTML, green success header, masked account number (last 4 only), amber "Didn't do this?" security callout, CTA to `/profile-step`
- `profile_microservice` gains a `NotificationService` client (scaffold) and `notification_service` + `internal_api_key` blocks in `config/services.php` — ready for future profile notifications without further plumbing
- `ProfileController::deleteUserBankAccount` captures bank name, account number, beneficiary, and timestamp before delete, then fires the notification via a best-effort helper (failures logged, do not affect the delete response)
- Company branding (`{{company.name}}`, `{{company.support_email}}`, `{{company.website_url}}`) resolved from tenant settings at dispatch time; user-facing CTA URL built from `tenants.frontend_url`
- **New env vars required on profile_microservice:** `NOTIFICATION_SERVICE_URL`, `NOTIFICATION_SERVICE_TIMEOUT`, `NOTIFICATION_SERVICE_ENABLED`, `INTERNAL_API_KEY`. Must share `INTERNAL_API_KEY` with the notification microservice