Unreleased
Added
  • RBS trigger log filtering and drill-down on the rule view — scope toggle between the rule's own matches (new default) and all evaluations of its trigger (child triggers included, with a Matched column and dimmed non-matching rows); decision / phase / dry-run / date-range filters and server-side pagination; rows expand to show the full event id (copyable), evaluated trigger, matched rule ids, exact time with seconds, and the context / actions-returned JSON. Filter-phase log rows now render correct labels (phase "Filter"; decisions Filter Applied / Filter (Dry Run) / No Filter Matched — previously showed rule-level "Always" and grey unknown-decision badges). Payment method ids are resolved to display names in the show_payment_methods action card and in the trigger log reason column, with #id fallback for methods no longer in the catalog rbs admin_ui
    Deploy: pull rbs, admin_ui, ui_gateway (independent order, no migrations), then view:clear on admin_ui. Log rows written before the deploy keep a NULL user (see the user context fix below) and still render fine.
  • Mobile deep linking phase 2 — return_mode: "app" on deposit-submit now covers every hosted-checkout PSP: new public app-return routes for EPG, ARP Digital, MyFatoorah (return + error) and Match2Pay (outcome bounce only; crediting stays webhook-based) reusing each PSP's existing public finance confirm with user_id embedded in the return URL (same pattern as the web returns; no finance changes). Plus verified deep-link scaffolding on ui_gateway for the auth email links: /.well-known/assetlinks.json and /.well-known/apple-app-site-association served from per-tenant env (MOBILE_ANDROID_PACKAGE, MOBILE_ANDROID_CERT_SHA256, MOBILE_IOS_TEAM_ID, MOBILE_IOS_BUNDLE_ID; both routes 404 until set) covering /reset-password, /verify-email, /demo-verify-email, /demo-signup-activate. OpenAPI spec bumped to 1.4.1.0. Blocked on the Flutter developer supplying the app identifiers api_gateway ui_gateway
  • Mobile deep linking for card deposits (N-Genius v1) — the Flutter app sends return_mode: "app" on deposit-submit; the gateway points the PSP redirect at its new public GET /{tenant}/deposit/ngenius/app-return route (throttled, API-key exempt like webhooks), which confirms the order via the new public finance ngenius/app-confirm (user derived from the invoice, state verified server-to-server with N-Genius) and renders a bounce page redirecting to {tenant.app_scheme}://deposit/return?ref=..&status=success|failed|cancelled with a manual "Return to app" fallback button. New nullable tenants.app_scheme column (gateway DB, migration on migration-service; null = feature off) — set dev tenant to atariaapp after deploy. Web flows unchanged; password-reset app handover deferred to verified App/Universal Links (the reset token is a secret; custom schemes can be squatted). OpenAPI spec bumped to 1.4.0.0. See documents/API-Gateway/MOBILE_DEEP_LINK_PLAN.md finance_microservice api_gateway migration-service
    Deploy order: 1) migration-service — run the gateway migration: php artisan migrate --path=database/migrations/gateway --database=gateway; 2) finance_microservice; 3) api_gateway, then route:clear/config:clear. Set the dev tenant's scheme: UPDATE tenants SET app_scheme = 'atariaapp' WHERE slug = '{dev-slug}' (and cache:clear on api_gateway — tenant rows are cached 10 min). Sandbox-test one card payment from the app before telling the Flutter dev. Per-tenant onboarding: every white-label tenant that ships a mobile app needs its own unique scheme — same UPDATE with that tenant's slug and scheme (e.g. UPDATE tenants SET app_scheme = 'okmapp' WHERE slug = 'okm'), then cache:clear on api_gateway. Tenants without a mobile app stay NULL (feature off; bounce page shows a plain "return to the app" message). Add this step to the tenant onboarding checklist. Never reuse one scheme across two tenants — two tenant apps on the same phone would collide.
  • Client OpenAPI spec catch-up (docs only, 1.3.2.0) — documents changes that shipped undocumented: amount and fields=ids parameters on GET /payment-methods with the visibility-rule refetch guidance, submit-time rejection notes on deposit/withdrawal endpoints, the ARP and MyFatoorah return-confirm endpoints, and the demo-signup flow (/preliminary-group/list, /demo-signup, /demo-signup-activate). ui_gateway /api-docs copies and user guide synced api_gateway docs ui_gateway docs
  • RBS payment method visibility rules — admins can now control which payment methods users see and can use, based on rule conditions (e.g. amount between 10 and 2500 shows only the selected methods). New "filter" rule phase with three triggers (Allowed Payment Methods, Deposit Payment Methods, Withdraw Payment Methods) and a Show Selected Payment Methods action; the gateway strips disallowed method ids from GET /payment-methods (fail-open), and the same rules are enforced at deposit/withdraw submit so hidden methods cannot be submitted directly. Phase wording across the admin UI changed to Before Trigger / After Trigger / Before & After, with an Always badge for filter rules; the phase selector is now driven by each trigger's supported phases, and rule create/update validates phase and action types against the catalog. Deposit and withdraw pages refetch the method list as the amount changes using a new ids-only projection (GET /payment-methods?fields=ids) so method instructions/config are not resent per keystroke rbs api_gateway admin_ui ui_gateway finance_microservice
    Deploy order: rbs → api_gateway → admin_ui → ui_gateway, then config:clear/cache:clear per service. No migrations. Create the first rule in dry-run and watch rule_trigger_log (filter / dry_run_filter decisions) before switching to enforce. A newly created first filter rule can take up to 5 minutes to apply (has-rules cache). Stricter rule validation note: editing an existing rule whose phase its trigger doesn't support now requires correcting the phase. See documents/RBS/PAYMENT_METHOD_VISIBILITY_PLAN.md.
  • Dedicated GET /api/{tenant}/payment-methods client endpoint — lists active payment methods with the same optional direction=deposit|withdrawal filter as /get-account; built for the Flutter deposit/withdraw screens. The payment_method field bundled in GET /get-account is now deprecated (still returned) and will be removed once the Flutter developer confirms migration — OpenAPI spec bumped to 1.3.0.0 with both changes marked [contract] finance_microservice api_gateway
  • Admin notifications now also send by email — when an admin has an admin event enabled in their bell, the same event is delivered by email too (no separate opt-in; the existing per-event subscription drives both channels). Email is additive and best-effort: admins with no address on record still get the bell, and email rides the global EMAIL_CHANNEL_MODE gate so it stays dormant until the email cutover flips the mode (the new template is not exempt). Delivery uses one generic template generic_admin_email rendering the bell title/body (no deep link in v1 — the email tells the admin to log in to review); the delivery report still shows the real event slug per email row. Both generic system templates (generic_in_app, generic_admin_email) are now hidden from the admin template listing notification migration-service admin_ui
    Deploy: 1) migration-service — seed the new template for every active tenant: php artisan tenant:seed {slug} --class=AdminNotificationEmailTemplateSeeder (idempotent; or re-run DefaultEmailTemplateSeeder / OkmEmailTemplateSeeder, which now include it); 2) notification — deploy and restart the queue workers; 3) admin_ui — copy change only. Until the template is seeded on a tenant, the email leg silent-skips (no_active_template) and the bell still fires. Admin email does not actually deliver until EMAIL_CHANNEL_MODE is test or on — test with a redirect inbox first. See documents/ADMIN_EMAIL_NOTIFICATIONS_PLAN.md.
  • Secret-bearing emails migrated to the template system (Batch 10 + guards) — the 9 events that deliver system-generated secrets (admin login / withdrawal / bank-method / crypto-method OTPs, MT5 live+demo account credentials, MT5 password changes, admin invite temp password) plus the admin-invite verification email now send through the notification service; zero direct Mail:: sends remain on any microservice branch notification migration-service admin_backend finance_microservice account_microservice auth_microservice profile_microservice
    Deploy order is a hard requirement: 1) notification service (guards + the tenant-aware queue fix) and rebuild the notification worker containers; 2) seed the templates (below) for every active tenant; 3) the five microservice branches. Deploying the microservices before the templates are seeded means OTP / credential / invite emails silent-skip — nobody receives them. No tenant migration required — this release adds no schema; the only DB step is the template seeder. Run on migration-service per tenant: ```bash php artisan tenant:seed {slug} --class=DefaultEmailTemplateSeeder # Default-brand tenants php artisan tenant:seed {slug} --class=OkmEmailTemplateSeeder # OKM-brand tenants ``` Idempotent and safe on already-seeded tenants: templates with unchanged content are untouched (SHA-256 version dedup); only the 10 new Batch 10 slugs are created (email template count ~74 → 84). Each seeder refuses to run against a tenant whose system.email_brand doesn't match. Caveats: admin-customised bodies are reset to the stock version (as a new version — the custom one stays in history; old notifications keep their pinned versions), and any deactivated seeder-owned template is re-activated. Guards (notification): required_template_placeholders — template saves missing their secret placeholders ({{otp}}, {{main_password}}, {{invest_password}}, {{new_password}}, {{password}}, and retro-protected {{reset_url}}/{{verify_url}}) are rejected 422; secret_template_variables — secret values are masked (••••••) in the stored notifications row after delivery, so credentials never sit readable in the DB or admin log. Masked rows re-render masked (deliberate). email_exempt_slugs synced with the 2026-06-13 call-site audit: all 20 cut-over events plus the 10 secret slugs now bypass EMAIL_CHANNEL_MODE (they have no legacy fallback / must always deliver for real). Previously only 2 were listed — off/test mode would have silenced 18 events. Semantics preserved: OTP and invite-resend endpoints return 500 when the queue submission fails (user/admin retries), as legacy did. OTP delivery is now queued — verify worker latency (created_at → sent_at) after deploy. Dormant code deleted: profile's AccountController + RegisterController and account's RegisterController (unrouted dead copies with unreachable credential sends). See documents/BATCH9_SECRET_EMAILS_PLAN.md for the full plan + implementation deltas.
  • API key scope enforcement: new apikey.scope:{client|admin} middleware on all tenant routes — flutter/web keys are limited to client endpoints, admin keys to admin panel endpoints; follows VALIDATE_API_KEY_MODE (off = no-op, log = warn only, enforce = 403); keys without scopes behave as * so existing minted keys are unaffected. Dev tenant verified end-to-end under enforce: web/admin/flutter keys minted with scopes, both UIs working, flutter key 403-blocked on admin routes api_gateway migration-service
  • admin_ui now sends X-API-Key on all gateway requests — the config entry existed but no service attached the header, so enforce mode would have broken the whole admin panel admin_ui
  • OpenAPI 3.0 spec for the client API — documents/API-Gateway/openapi-client.yaml documents all 66 client-scope endpoints (Auth, Profile/KYC, Accounts/MT5, Finance, Tickets, Notifications, Public) with request fields sourced from the downstream microservices' validation rules; covers the two-layer auth model (X-API-Key + Bearer) and the success-flag response convention; lints clean (Redocly) and imports directly into Postman / Swagger UI — this is the Flutter developer handover document api_gateway
  • Client API documentation for the Flutter dev — a single Stripe-style page at GET /api-docs (ui_gateway) that merges the **functional guide and the API reference**: for each feature (auth, MT5 accounts, funding, deposits, withdrawals, transfers, profile/KYC, payout methods, promotions, notifications, tickets, public config) it shows the prose explanation and the rules, then the endpoints that power it (params, request bodies, responses, examples). Custom dependency-free renderer (no Swagger/Redoc), sidebar nav, downloadable spec (OpenAPI YAML + JSON), behind an HTML login form (DOCS_AUTH_USER / DOCS_AUTH_PASSWORD; 404 if unset). Prose source resources/docs/user-guide.md (rendered via Str::markdown); endpoints from the OpenAPI spec copy. Documents the payment-method gap (no dedicated endpoint — read payment_method from GET /get-account with ?direction=deposit|withdrawal). /user-guide redirects here api_gateway ui_gateway
    Deploy (ui_gateway) for the /api-docs page: 1. git pull origin ui_gateway 2. Set the docs login in .env (both required, or the page 404s): DOCS_AUTH_USER, DOCS_AUTH_PASSWORD. 3. php artisan route:clear && php artisan config:clear && php artisan view:clear 4. Reload the worker (systemctl reload php-fpm or restart the container) so the new controller/middleware classes load. Dependency: the page renders Markdown via Str::markdown (league/commonmark, ships with Laravel). If it errors on a missing CommonMark class, run composer require league/commonmark once on ui_gateway. No other composer changes. Verify: /api-docs prompts for the login, then renders feature sections (prose + endpoints) with a working sidebar; /user-guide redirects to /api-docs.
  • API request audit trail — AuditApiRequest middleware logs one metadata-only JSON line per gateway request to a dedicated daily apilog channel in storage/logs/api-audit/ (request id, api key id + client type, tenant, method, path, status, ip, user agent, duration). Runs first in the tenant stack so rejected requests (401/403/429) are recorded too; echoes X-Request-Id for correlation. Never logs request/response bodies. Retention via API_AUDIT_RETENTION_DAYS (default 90) api_gateway
  • Notification email templates — Batch 8 (remaining P3/P4), completing the Default email-template track (73 templates): 11 templates in Batch8RemainingEmailTemplateSeeder — security alerts (suspicious_login, admin_suspicious_login, admin_impersonation_alert), profile_updated, document_remarks_added, the 4 IB partner notifications (ib_new_referral, ib_referral_activated, ib_referral_stats_updated, ib_commission_paid), and the redesigned documents_required + compliance_alert migration-service
    Re-seed templates per tenant: php artisan tenant:seed {slug} --class=DefaultEmailTemplateSeeder (idempotent via SHA-256 content-hash dedup). documents_required and compliance_alert were rebuilt from one-line stubs to the full Default style — re-seeding creates a new version of each (old notifications keep their pinned version). Deferred: new_promotion_available / promotion_status_changed (P4 marketing broadcasts) — no template until the broadcast composer + opt-in system exist. Email template track complete. Remaining work is not authoring: OKM ports (3/~64), call-site wiring on the microservice branches, and the by-design Blade-only system-secret emails. No call-site wiring in this change.
  • Notification email templates — Batch 7 (support + admin lifecycle): 8 Default-styled templates (ticket_created, ticket_replied, ticket_created_by_admin, admin_ticket_assigned, admin_ticket_comment, admin_email_verified, admin_role_updated, admin_account_removed) in Batch7SupportAdminEmailTemplateSeeder migration-service
    Re-seed templates per tenant: php artisan tenant:seed {slug} --class=DefaultEmailTemplateSeeder (idempotent via SHA-256 content-hash dedup). OKM tenants render these with the Default body until OKM ports are authored. admin_invite excluded — the employee-credentials email delivers a temporary password; stays on its Blade path, never admin-editable (same rule as the OTP / credential exclusions). Admin login OTPs (_otp / _whatsapp / _sms) are excluded too; admin_password_reset_link / _success already shipped in Batch 1. No call-site wiring — templates authored only. Five of these are admin-addressed (recipient_type='admin').
  • Notification email templates — Batch 6 (MT5 admin actions): 9 Default-styled templates (leverage_updated, mt_manual_deposit, mt_manual_withdrawal, mt_account_deleted, mt_account_unlinked, mt_account_transferred, mt_account_disabled, mt_account_request_approved, mt_account_request_rejected) in Batch6Mt5AdminEmailTemplateSeeder migration-service
    Re-seed templates per tenant: php artisan tenant:seed {slug} --class=DefaultEmailTemplateSeeder (idempotent via SHA-256 content-hash dedup). OKM tenants render these with the Default body until OKM ports are authored. mt5_password_changed_by_admin excluded — carries the new MT5 password; stays on its account_microservice Blade path, never admin-editable (same rule as the OTP and MT5-credential exclusions). No call-site wiring — templates authored only.
  • Notification email templates — Batch 5 (admin-initiated user actions): 11 Default-styled templates (admin_created_account, account_blocked, account_unblocked, account_deactivated, account_deleted, password_reset_by_admin, wallet_credited_by_admin, wallet_debited_by_admin, internal_transfer_by_admin, profile_updated_by_admin, document_uploaded_by_admin) in Batch5AdminActionsEmailTemplateSeeder migration-service
    Re-seed templates per tenant: php artisan tenant:seed {slug} --class=DefaultEmailTemplateSeeder (idempotent via SHA-256 content-hash dedup). OKM tenants render these with the Default body until OKM ports are authored. Credential-free by design. admin_created_account uses an activation link and password_reset_by_admin uses a reset link — neither emails a temporary password, keeping them inside the secret-exclusion boundary (same principle as the OTP / MT5-credential exclusions). No call-site wiring — templates authored only.
  • Notification email templates — Batch 4 (crypto + internal transfers): 6 Default-styled templates (withdrawal_submitted_crypto, internal_transfer_submitted, internal_transfer_success, internal_transfer_rejected, transfer_comment_added, promotion_claimed) in Batch4CryptoTransferEmailTemplateSeeder migration-service
    Re-seed templates per tenant: php artisan tenant:seed {slug} --class=DefaultEmailTemplateSeeder (idempotent via SHA-256 content-hash dedup). OKM tenants render these with the Default body until OKM ports are authored. Credential-bearing account emails excluded. live_account_created, demo_account_created, and mt5_password_changed carry system-generated MT5 passwords and are NOT templates — they stay on their account_microservice Blade paths, never admin-editable (same rule as the OTP emails). No call-site wiring — templates authored only. Added a gift icon variant to BuildsEmailTemplates for promotion_claimed.
  • Notification email templates — Batch 3 (KYC + payout destinations): 9 Default-styled templates (bank_account_added, bank_account_updated, crypto_address_added, crypto_address_updated, crypto_address_deleted, kyc_approved, kyc_rejected, document_status_changed, document_uploaded) in Batch3KycPayoutEmailTemplateSeeder; shared body builders extracted into a BuildsEmailTemplates trait migration-service
    Re-seed templates per tenant: php artisan tenant:seed {slug} --class=DefaultEmailTemplateSeeder (idempotent via SHA-256 content-hash dedup). OKM tenants render these with the Default body until OKM ports are authored. Document-upload confirmations are one generic template. document_uploaded takes a {{document_type}} variable instead of four per-type slugs (document_uploaded_identity / _address / _networth / _signature). Call sites send document_uploaded + a type string. Payout-destination changes (bank/crypto add/update/delete) are P1 security alerts — each carries a "Didn't do this?" callout. Excluded: bank/crypto method add-OTPs (system OTP, Blade path); admin-initiated doc/profile events (Batch 5). bank_account_deleted was already shipped and is unchanged. Refactor: the shared shell/icon/heading/detailPanel/callout/button/greeting builders moved into BuildsEmailTemplates; Batch 2 was refactored to use it (rendered output unchanged). No call-site wiring — templates authored only.
  • Notification email templates — Batch 2 (money movement): 9 Default-styled templates for the fiat deposit/withdrawal lifecycle (withdrawal_submitted, withdrawal_approved, withdrawal_rejected, withdrawal_comment_added, deposit_submitted, deposit_approved, deposit_rejected, deposit_comment_added, deposit_received_psp) in Batch2MoneyEmailTemplateSeeder, aggregated by DefaultEmailTemplateSeeder migration-service
    Re-seed templates per tenant: php artisan tenant:seed {slug} --class=DefaultEmailTemplateSeeder (idempotent via SHA-256 content-hash dedup). OKM tenants render these with the Default body until OKM ports are authored. withdrawal_confirmed removed — it was a test-only stub. withdrawal_approved is the real withdrawal-approval template. The row persists on already-seeded dev tenants (template hard-delete is FK-restricted); deactivate or delete it manually there if desired — no call site sends it. withdrawal_otp excluded (system-generated OTP, stays on its finance Blade path). Crypto withdrawal + internal transfers deferred to Batch 4. No call-site wiring in this change — templates authored only; send call sites remain to be wired on the microservice branches.
  • Notification email templates — Batch 1 (auth core): 5 new Default-styled templates (password_changed, demo_email_verification, demo_welcome, admin_password_reset_link, admin_password_reset_success) authored in a self-contained Batch1AuthEmailTemplateSeeder, aggregated by DefaultEmailTemplateSeeder migration-service
    Re-seed templates per tenant to pick these up: php artisan tenant:seed {slug} --class=DefaultEmailTemplateSeeder. Idempotent — re-running adds only the new slugs; unchanged templates keep their existing version via SHA-256 content-hash dedup, so no spurious versions are created. OKM-branded tenants also run --class=OkmEmailTemplateSeeder; the 5 new slugs render with the Default-styled body until OKM ports are authored. admin_login_otp is intentionally NOT a template. The login OTP is generated and validated server-side, so its email body must not be admin-editable. It stays on its legacy Blade path on admin_backend (emails/otp.blade.php). No call-site wiring in this change — these templates are authored only. The events that send them remain to be wired on their respective microservice branches (separate track). Seeder structure going forward: each future batch (2-8) ships as its own provider class merged by DefaultEmailTemplateSeeder. The four already-shipped auth templates stay defined in DefaultEmailTemplateSeeder to keep their content hash stable.
  • API key validation foundation — per-tenant api_keys table (hashed SHA-256), three-mode middleware (off / log / enforce) on api_gateway, and CLI tooling for minting/listing/revoking keys migration-service api_gateway
    Tenant DB migration required. Run php artisan tenant:migrate {slug} for every active tenant. Adds api_keys table with hashed key storage, client_type, scopes, expiry, revocation, and last-used tracking. Default mode is off — this release is operationally a no-op until the env flag is flipped. Set VALIDATE_API_KEY_MODE=off in api_gateway .env (or leave unset) to keep current behaviour. The middleware is attached to all tenant routes but short-circuits when mode is off. Rollout sequence per tenant (see OPERATIONS_RUNBOOK.md §12 for full commands): 1. Mint three keys per tenant via php artisan apikey:create {slug} --type={web|admin|flutter} --name="..." on migration-service. Each key's plaintext is shown once. 2. Set API_GATEWAY_KEY on each tenant's ui_gateway, admin_ui, and Flutter build to the matching key. config:clear after. 3. Flip api_gateway to VALIDATE_API_KEY_MODE=log and watch logs for apikey.missing / apikey.invalid for ~3-7 days. 4. Once clean, flip to VALIDATE_API_KEY_MODE=enforce. Exempt routes (no key required): POST /{tenant}/webhook/match2pay, /{tenant}/ibp/*, /up. Rollback is VALIDATE_API_KEY_MODE=off + config:clear on api_gateway. No code redeploy needed. Existing X-API-Key header is now real. ui_gateway and admin_ui have always sent X-API-Key: {API_GATEWAY_KEY} but it was unenforced. Once mode is flipped to enforce, the env value must match a real key in the tenant's api_keys table or every request returns 401.
    • New `App\Models\ApiKey` (read-only) with `isActive()` / `status()` helpers; queried via `ApiKey::on($connectionName)` against the tenant DB resolved by `TenantConnectionService`.
    • `ValidateApiKey` middleware rewritten with three-mode switch on `services.validate_api_key_mode`. Caches validation results for 5 min keyed on `apikey:sha256:{hash}`. Updates `last_used_at` / `last_used_ip` debounced to once per minute per key (no write storm under load).
    • Middleware alias `validate.api.key` re-registered in `bootstrap/app.php`. Attached to both tenant route groups (subdomain-based and `{tenant}` path-prefix). Webhook and IBP groups stay exempt by virtue of being separate route groups.
    • `apikey:create {tenant} --type={web|admin|flutter} --name="..."` — generates `ata_{env}_{type}_{32hex}` format key, prepended-prefix stored separately for admin/log identification, prints plaintext exactly once with explicit "save now" warning.
    • `apikey:list {tenant} [--show-revoked]` — tabulates id, prefix, type, name, status (active/revoked/expired), last used, last IP, created.
    • `apikey:revoke {tenant} {id|prefix}` — sets `revoked_at`. Reminds operator to `cache:clear` on api_gateway for instant effect (otherwise 5-min cache window applies).
    • Super-admin panel CRUD UI for keys (the artisan commands are temporary tooling until that ships).
    • Per-app scopes beyond `["*"]` (the column exists but every minted key gets `["*"]` for now).
    • Rate limiting per API key (orthogonal — can be added later via existing throttle middleware keyed on `key_id`).
    • Per-install device registration for Flutter (not needed; bundled key is acknowledged as low-trust, Sanctum bearer is the auth boundary).
  • Referral attribution at signup via IBP bridge — public pages capture ?ref= into a 60-day encrypted cookie, register-submit verifies the code through the existing IBP outbound channel (3s timeout, no-op when bridge unconfigured), and the new user row is stamped with referred_by_id + referral_code_of_ib. The existing pushUserRegistered IBP push now carries the resolved (referral_id, referral_code) pair so IBP can link the user to the IB's wallet. Verify failures surface in a new admin "IBP Logs" page (logs.ibp.view permission) and trigger a rate-limited alert email (configurable via IBP_FAILURE_ALERT_EMAIL, default ataria@4xportal.com, max one per tenant per 5 min). Demo register captures the cookie but does not attribute the signup. migration-service admin_backend auth_microservice api_gateway ui_gateway admin_ui
    Tenant DB migration required. Run php artisan tenant:migrate {slug} for every active tenant. Adds referred_by_id and referral_code_of_ib to users, plus the logs.ibp.view permission key. IBP_FAILURE_ALERT_EMAIL env optional on admin_backend. Defaults to ataria@4xportal.com if not set. Override via env to redirect alerts elsewhere. config:clear after change. Bridge must be configured per tenant for any of this to fire. With no active row in ibp_bridges, the verify call short-circuits with skipped: true, attribution stays null, and no IBP HTTP calls are made (same guard as the existing pushUserRegistered path). Configure via IbpBridgeSeeder per documents/API-Gateway/ibp-bridge-setup.md.
  • **Lead Pipeline Board** — new drag-and-drop board on leads/pipeline-board showing leads across 5 columns (New, Preliminary, Demo, Marketing, Hot). Powered by SortableJS with forceFallback: true for cross-browser compatibility. Dragging a card between columns updates the lead's LeadType via POST /leads/pipeline/update-stage/{id}. Hot column is read-only (leads appear here based on status, not type); drops are blocked. Board renders dynamic KPIs (Total Leads, Total Value, Est Revenue Lost, Conversion Rate) computed from the full lead list. Horizontal scroll with edge detection. Pipeline stage changes are persisted to the backend and reflect in the leads directory on next load. (admin_ui, LeadsController)
  • **Lead Edit Page** — full-page edit at leads/edit/{id} (replaces the old modal approach). Pre-populates all fields from the lead's current data via GET /leads/edit/{id}/json. Lead type radio buttons are displayed but disabled (type is locked after creation). Existing notes are shown as a numbered list; new notes are appended via the existing_notes JSON array. Trading questions partial (trading_questions_edit.blade.php) pre-selects radio/checkbox values from the lead's trading array. Form submits via AJAX to POST /leads/update/{id}. (admin_ui, LeadsController)
  • **Assign / Reassign Lead Drawer** — reusable right slide-in drawer (partials/assign_lead_drawer.blade.php) matching the existing sales-assign-leads-modal-overlay pattern. Accepts a server-rendered $salesAgents array (id, name, initials) passed from the controller; the agent dropdown is populated server-side, eliminating the need for an AJAX fetch on open. Features agent search/filter, chip-based selection with remove, assignment notes textarea, and Cancel/Confirm. Submits via POST /leads/update/{id} with assign_to and notes payload. Wired to open from:
    • Pipeline board card dropdown (Assign button)
    • All 5 leads directory tables — "Assign Now" links in the agent column and "Assign" in the row actions dropdown
    • Lead view sidebar — "Reassign Now" button (opens in reassign mode with updated title/label)
    • Sales Leads directory — "Assign Now" link in the agent column
    • Sales Teams view — "Assign Now" link in the team leads table
  • **Sales Agents Data Pipeline** — new getAgents() method on LeadsController and SalesController that loads employees via EmployeeService, resolves names and generates initials, and returns a [{id, name, initials}] array. Passed as $salesAgents to all views that include the assign drawer: 5 leads directory pages, pipeline board, 5 lead view pages, sales_leads, and team_view. admin_ui
  • **AJAX Tab Navigation for Leads** — sales section tabs (Overview, Leads Directory) and leads directory sub-tabs (All Leads, New Leads, Demo Leads, Preliminary Leads, Marketing Leads, Pipeline Board) load via AJAX without full page refresh. The
    is replaced with the target page's content; scripts found after the admin-notifications-bell.js marker in the parsed HTML are re-executed sequentially (external scripts loaded via loadExternalScript, inline scripts injected). Browser history is updated via pushState; back/forward navigation triggers popstate to reload the appropriate tab. admin_ui
  • **Pipeline Board Drag-and-Drop Fix** — SortableJS initialization on the pipeline board was wrapped in DOMContentLoaded, which doesn't re-fire when scripts are injected via AJAX navigation. Fixed by replacing the DOMContentLoaded listener with an IIFE (function() { ... })() so initialization runs immediately when the script executes, regardless of how the content was loaded. admin_ui
  • **CSV Import/Export for All Leads Directory Pages** — export routes and controller methods for All Leads, New Leads, Demo Leads, Preliminary Leads, Marketing Leads, and Agents/Teams. Export generates a CSV download via League\Csv\Writer with UTF-8 BOM. Import tabs removed from directory pages (replaced with Export CSV links); import functionality consolidated to the controller endpoint. (admin_ui, LeadsController)
  • **Wallet Management** — employee wallets controller fetches reward wallets from wallet/list API, enriches with employee data from EmployeeService. Wallet KPIs computed dynamically. Wallet table renders from controller data. Transfer and history modal drawers (slide-in pattern matching the assign drawer) wired to wallet action buttons. Wallet export via GET /wallets/export streams a CSV of reward wallets. (admin_ui, SalesController)
  • **Team Detail Data Binding** — SalesController::teamView() passes $teamLeads, $teamActivities, $topAgentsByConversion, $topAgentsByRevenue to the team view. Team leads table renders from controller data with dynamic agent/status badges. (admin_ui, SalesController)
  • **Client Data Binding** — overview, business_clients, and ib_clients pages bound to controller data. Business clients filtered by from_ib = false; IB clients filtered by from_ib = true. admin_ui
  • **Sales Overview Graph UI** — revenue trend, monthly comparison, deposit vs withdrawal, doughnut charts, and map highlights updated with unified #2563eb blue palette, dark tooltips, gradient fills, and dashed secondary lines. Duplicate HTML sections removed. CSS color palette unified across all sales components (#0062ff#2563eb, #ffa500#f97316). admin_ui
  • **Lead View Bindings** — lead view pages (activity, follow_ups, notes, lead_details, demo_details) bind dynamic data from the controller. Follow-up counts, activity logs, trading experience, demo account details, and company information all render from the normalized lead data. Edit button redirects to the full edit page. Status dropdown updates on click. (admin_ui, LeadsController)
Changed
  • RBS rule creation is disabled in production (APP_ENV=production) — the Create New Rule button on the rules listing stays visible but greyed out with an explanatory tooltip, admin_ui's create page redirects back to the listing and its store proxy returns 403, and the rbs microservice's POST /api/admin/rules itself returns 403 so the block also covers the gateway admin endpoint and direct API calls. Editing, toggling, and deleting existing rules remain available; rules should be created on dev/staging and promoted. Each service checks its own APP_ENV — run config:clear on admin_ui and rbs after deploy if config is cached admin_ui rbs
  • IB partner emails (ib_*) parked: IbpBridge routes are outbound pushes — recipients live in the IBP system, no local email address; product decision pending admin_backend docs
  • email:test-batch artisan command — fires every active email template with auto-generated sample variables through the real dispatch path, for Phase 1 validation and regression re-checks notification
  • Phase 1 template validation completed 2026-06-06: all 74 templates sent and signed off on dev via test redirect; checklist recorded in documents/PHASE1_VALIDATION_CHECKLIST.md api_gateway
  • EMAIL_CHANNEL_MODE gate (off/test/on) + EMAIL_TEST_REDIRECT for the template-email channel, with explicit-on fail-safe — missing/empty/unknown env behaves as off, never breaks; test redirects non-exempt email to the test inbox; already-live bank_account_deleted / crypto_address_deleted are exempt via notification.email_exempt_slugs notification
  • Tenant profile.company.* settings auto-merged into email variables at dispatch (call-site wins) so all 74 templates render {{company.*}} centrally — EMAIL_CUTOVER_PLAN Phase 0 notification
  • Notification email templates — Batch 9 (finance gaps): deposit_receipt_uploaded + deposit_expired, missed by accident between Batches 2 and 4 migration-service
  • Employees — hide Client Accessible action; Verify Email sends email dynamically admin_ui api_gateway admin_backend
  • MT5 tab — separate Live and Demo connection status badges driven by connection_status from admin.mt_setting.mt5.get admin_ui
  • 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
    • Lead edit flow changed from modal-based to full-page redirect — all 5 directory blades and the lead view scripts partial no longer include `edit_lead_modal.blade.php`; Edit buttons now redirect to `/leads/edit/{id}` via `window.location.href`
    • Pipeline board card dropdown Edit button changed from `<button>` to `<a>` tag linking directly to the edit route
    • Assign drawer agent list changed from AJAX-fetched to server-rendered — `assign_lead_drawer.blade.php` now accepts `$salesAgents` from the controller and renders agent items via `@forelse`; the JS script no longer calls `/admin/employees/list` on open
Removed
  • ib_commission_paid email template — no commission-payout event exists in the project; dropped from Batch 8 and soft-deleted on seeded tenants via Batch9FinanceGapEmailTemplateSeeder::deactivateRemovedTemplates() migration-service
Fixed
  • RBS: every rule match wrote two "Rule updated by system" audit entries (triggered_count / last_triggered_at) and cleared the per-trigger rules cache — flooding the audit trail on the rule view and largely defeating the 60s rule cache. Counter bumps are now a single quiet update that skips the observer, and the observer additionally ignores counter-only changes from any path rbs
  • RBS: the per-rule trigger log answered the wrong question — it listed every evaluation of the rule's trigger_type (including rows where only other rules, or none, matched) and, for a rule on a parent trigger, never showed its child-trigger firings (the log stores the fired child trigger id). Default scope now filters by matched_rule_ids; scope=all keeps the trigger-wide view including child triggers, with each row flagged matched_this_rule rbs
  • RBS filter phase evaluated with no user identity: the payment-methods listing calls sent no user_id and the gateway never authenticates ($request->user() is always null there), so rule_trigger_log rows for payment_methods.viewed.* had no user and — because country resolution starts from user_id — country/region conditions on payment method visibility rules could never match. ui_gateway now sends the session user id on both the funding page load and the amount-refetch call; gateway context builder unchanged ui_gateway
  • Security: payment method listings returned to clients (GET /payment-methods and the deprecated payment_method array in GET /get-account) exposed the full DB row including config_json, which on PSP kinds holds merchant credentials (N-Genius api_key/outlet ref, Comtrust EPG store/terminal/username/password) — and ui_gateway embedded it into the deposit page source. Rows are now a whitelisted projection (id, name, code, kind, direction, is_instant, instructions, image, display_order); config_json is returned only for kind = manual methods, where it holds the customer-facing transfer details (bank account, wallet address). Client OpenAPI spec bumped to 1.3.1.0 documenting the field set finance_microservice api_gateway docs ui_gateway docs
  • Admin-created clients and admin-opened live MT5 accounts are now pushed to the IB Portal — ClientController::store and createLiveAccount on admin_backend mirror the public-flow IBP pushes (auth/account microservices → /api/ibp/internal/*) with the same payloads; admin-created clients carry a null referral pair; best-effort with the same no-active-bridge skip guard, so an IBP outage never fails the admin action. Adds unit tests for IbpOutboundService (auth headers, endpoints, error logging) and feature tests for the store() push admin_backend
  • Security: removed debug Log::info calls across the API Gateway controllers and service classes (68 in total) that wrote full request data and response bodies to laravel.log — on the auth path these leaked user passwords (login/register/reset) and issued Bearer tokens in plaintext. Log::warning / Log::error (error handling, security events) are kept api_gateway
  • Security (IDOR): client endpoints derived *whose* data to act on from a caller-supplied user_id instead of the authenticated token, so any logged-in user could read or modify another user's data by changing user_id. New ForceTokenUserId middleware (force.user_id) overwrites user_id with $request->user()->id on each service's authenticated route group; backward compatible (the web UI already sends its own id), logs mismatches as apikey.user_id_override. Covers profile (verified on dev), finance, account, and helpcenter (whose downloadAttachment ownership check also compared against the tamperable user_id and now checks the token user). notification/auth were already token-derived; admin_backend is permission-gated and unaffected profile_microservice finance_microservice account_microservice helpcenter
    Deploy: git pull each of the four branches, then php artisan route:clear && php artisan config:clear (cached routes hold the old middleware list) and reload php-fpm if opcache is on.
  • Auth email links (verification, password reset, demo verification) are now built from the tenant's stored frontend_url (server-side, resolved by ResolveTenant) instead of a caller-supplied frontend_url request field. Direct API callers such as the Flutter mobile app never sent that field, so the link base fell back to config('services.frontend_url')/localhost and produced broken links; the web UI was unaffected because ui_gateway always sent its own URL. Also removes a client-controlled URL from security emails. frontend_url is dropped from the register-submit, forgot-password, and demo-register request bodies in the client OpenAPI spec (bumped to 1.2.1.0) and User Guide auth_microservice api_gateway docs ui_gateway docs
    Deploy: git pull on auth_microservice and reload php-fpm. Before deploying, confirm every active tenant has frontend_url populated in the gateway tenants table (dev, demo, zyz, okm, and any future tenants). It is a nullable column; a NULL value now falls back to config('services.frontend_url')/localhost, which would break web links too, not just mobile.
  • Client OpenAPI: GET /settings documented as a plain authenticated request — removed the keys[] query parameter (the gateway forwards an empty payload, so the filter never reaches the auth microservice and the full map is always returned), and clarified that a bare GET with X-API-Key + Bearer returns the whole tenant settings {key: value} map api_gateway docs ui_gateway docs
  • Client OpenAPI: added a consumer-facing ## Changelog section inside the spec info.description (travels with the yaml, so it shows in Postman / Swagger / the hosted docs page). Entries are tagged [contract] (re-import the spec) vs [docs] (wording only), keyed to info.version; seeded at 1.2.0.0 (baseline) and 1.2.1.0. Going forward, every client-spec change bumps info.version and adds a changelog line in the same edit api_gateway docs ui_gateway docs
  • Hosted docs page (/api-docs) now renders that spec changelog as its own "Changelog" section with a sidebar link — ApiDocsController extracts the ## Changelog block from the spec's info.description (the page is built from the User Guide and previously ignored info.description, so the changelog was browser-invisible). No new deploy step beyond the existing docs-page deploy ui_gateway
  • ui_gateway AuthService::buildHeaders() override dropped X-API-Key, blocking login under enforce mode; now reuses base headers ui_gateway
  • OKM email theming: BuildsEmailTemplates is theme-parameterized (Default output byte-identical, hash-verified) with a full OKM skin (logo header, Dear-greeting + assurance copy, SECURITY ALERT boxes, navy compliance footer); OkmEmailTemplateSeeder renders all 74 templates in OKM branding (3 hand-authored + 7 regenerated + 64 themed); system.email_brand tenant setting + OkmEmailBrandSeeder + brand guards prevent wrong-brand re-seeds; validated on dev incl. full OKM→Default round-trip migration-service
  • Templated email wired at 69 trigger points (Phase 2 waves 1–6, all dev-verified in test mode; legacy mailables untouched until cutover): finance 7 (withdrawal/deposit/transfer submissions, PSP receipt, receipt upload, expiry), profile 10 (password/profile changes, 4 doc uploads → generic document_uploaded, bank/crypto add+update), account 1 (promotion_claimed), helpcenter 1 (ticket_created), auth 6 (signup funnel with legacy-identical links), admin_backend 44 (money decisions, compliance/KYC, client actions, MT5/wallet admin, support + admin lifecycle incl. first admin-recipient emails) finance_microservice profile_microservice account_microservice helpcenter auth_microservice admin_backend
  • email:test-batch validation run signed off all 74 templates 2026-06-06 notification
  • Clients bulk Permanently Delete — fixed 500; uses same cleanup as row action admin_backend
  • Compliance KYC list and pending-documents count now exclude soft-deleted users (status = deleted), matching the Clients list admin_backend
  • Admin footer no longer sits under the sidebar on Notifications, Log History, IBP Logs, and Reports — pages keep
    open like Support/Funds admin_ui
  • fixed filter and graphs in (admin_ui and microservices/notification)
  • Pipeline board drag-and-drop not initializing when navigating to the Pipeline Board tab via AJAX — SortableJS init code was inside a DOMContentLoaded handler that never re-fires after AJAX content injection; replaced with an IIFE
  • Assign drawer agent dropdown not populating — controller now passes $salesAgents to all views including the drawer partial; partial renders agents server-side with @forelse loop
  • Edit button on pipeline board cards not working — changed from a non-functional
  • Assign Now links and Assign buttons in leads directory tables not wired — added leads-assign-trigger class with data-lead-id and data-lead-name attributes, wired to window.openLeadsAssignDrawer()
v1.2.0.0 2026-06-11
Added
  • Saved Reports module — admin-facing landing page with six CRM reports (Client, KYC / Compliance, Support Ticket, Fund, Internal Transfer, Account), each with filter modal, Recent table, summary cards + donut + 30-day sparkline, paged rows, PDF export (5k row cap), CSV export (10k row cap), and Re-run migration-service admin_backend api_gateway admin_ui
    Tenant DB migration required. Run php artisan tenant:migrate {slug} for every active tenant. Adds the generic reports table (id, name, type, filters JSON, columns JSON, admin_id FK → admin_register, row_count, status, timestamps; indexed on (type, created_at)). One table backs all five report types — adding a new report type does NOT require another migration. Composer dependency. barryvdh/laravel-dompdf:^3.0 is now in composer.json on admin_backend. Run composer install on the admin_backend server after pulling. Required for PDF export — the controller fails with "Class Pdf not found" without it. Permission keys. Reports use the shared reports.* namespace (no per-report-type split): reports.view, reports.generate, reports.rerun, reports.delete, reports.export_pdf, reports.export_csv. Grant these to admin roles that need access; absence hides the buttons via ui.perm middleware on admin_ui. Export throttles per admin. admin_backend: throttle:5,1 (PDF), throttle:10,1 (CSV). api_gateway: throttle:30,1 (PDF), throttle:60,1 (CSV). admin_backend is the tighter gate (matches "real" load); api_gateway's looser caps absorb cancelled-redirect retries. PDF hardening. dompdf runs with isRemoteEnabled=false, all variables in PDF blades go through {{ }} (never {!! !!}), and safeLogoUrl() accepts only data:image/* or local public/ / storage/ paths — no remote logo URLs. Logo source is the per-tenant settings row keyed profile.company.logo_url. CSV hardening. Streams via php://output with UTF-8 BOM. Cell values prefixed with =, +, -, or @ are escaped with a leading single-quote to defeat formula injection in Excel / Google Sheets.
    • Generic per-tenant `reports` table with a `type` column distinguishing `client_report` / `kyc_report` / `support_report` / `fund_report` / `internal_transfer_report` / `account_report`. Filter set + selected columns + row count snapshotted at Generate time; Re-run refreshes the row count without re-prompting for filters.
    • Four-branch shape per report: admin_backend owns the Eloquent queries (`{X}ReportService::buildQuery / summary / rows / exportRows`) and the PDF/CSV stream (`{X}ReportController`); api_gateway proxies all 9 endpoints per report (7 JSON + PDF + CSV) plus a `streamBinary` helper for the export passthrough; admin_ui has a single `ReportController` covering all six report types and one `routes/report.php` registering 8 routes per report.
    • Cross-service identifier consistency: country = numeric `countries.id`, region = `regions.region_code` string. Both Service classes and admin_ui dropdown `<option value>` use the same form so saved filters match the evaluation query.
    • Audit logging: every Generate / Re-run / Delete / Export call writes an `AdminLogService` row.
    • **Client Report** — defaults to live (`account_type IN ('live', 'real')`); `last_login` uses `users.updated_at` as proxy (no real `last_login_at` column yet); funded check reads `transactions` (`type='deposit' AND status IN ('completed','approved','success','paid')`), not `invoices`. CID-prefixed identifiers.
    • **KYC Report** — 8 filters including KYC Status, Email Verified, Doc Category, Document Status. Decision Date / Decided By columns use proxy logic (`MAX(documents.approved_at)` and latest `documents.approved_by`) — no `users.compliance_decided_at` migration was added; can switch to precise once that lands.
    • **Support Report** — 3-state status model (Open / In Progress / Done) derived at projection time from raw `(status, assign_now)`. WOO-prefixed identifiers. Resolution Time is `updated_at - created_at` proxy (no `closed_at` column).
    • **Fund Report** — case-insensitive `LOWER(type)` matching (production data is mixed-case `deposit` / `Deposit` / `Withdrawal`). Approved bucket pulls `approved/completed/success/paid` for parity with Client funded check. Excludes Internal Transfer via `WHERE LOWER(type) NOT IN ('internal transfer', 'transfer')`. Volume aggregation is count-based not currency-summed (no FX service); admin filters by Currency for clean per-currency totals. TXN-prefixed identifiers.
    • **Internal Transfer Report** — inverse Fund filter (`WHERE LOWER(type) IN ('internal transfer', 'transfer')`). Type filter and Payment Method drop out (locked / no invoice); From Account + To Account text filters added. `from_account` / `to_account` shown raw (typically MT5 login digits or "wallet"), no normalisation.
    • **Account Report** — first report keyed on `accounts` (MT5 listing) rather than users / transactions. `accounts.type` integer maps `1 → Live, 2 → Demo, 3 → IB` per the migration comment. Group + Leverage resolved at projection by joining `groups` (Live / IB) or `demo_groups` (Demo) based on the row's type; display name prefers `mask_group_name` with `mt5_group_name` fallback. `master_password` + `invest_password` are never projected (security boundary). No always-applied filter — covers every row by default. Donut shows Live / Demo / IB breakdown. Balance / equity / margin deliberately out of scope (live MT5 state, Phase 8 reports). ACT-prefixed identifiers; raw MT5 login also kept as `account_no` column.
    • **Promotion Report** — promotion-centric (one row per campaign with `claim_count` aggregated via `withCount('users')`); the data model has no separate claims table — a claim is a `users.promotion_id` stamp. Status derived from `is_active` + `start_date` + `end_date` vs `NOW()` → 4 states (Active / Inactive / Expired / Scheduled); filter compiles to matching SQL predicates so result-set agrees with classifier. Total Claims is a single sub-query (`count(users) WHERE promotion_id IN filtered_ids`); claims-per-day sparkline scopes to the same id set. Filter key is `promo_status` (not `status`) so it doesn't clash with the shared user-status dropdown (different valid-value sets). Payouts deliberately out of scope (no `bonus_amount` column on promotions or users). PRM-prefixed identifiers; `groups_count` shows JSON-array length, not the IDs themselves.
    • **Admin Activity Report** — audit-log centric (one row per `admin_logs` entry joined to `admin_register`); reads back the trail that every other controller writes via `AdminLogService::log()`. Action filter is contains-search (`LOWER LIKE`) since actions are freeform strings — no curated enum. Admin filter key `admin_actor` (uses admin id, distinct from Support Report's `assigned_to` which stores name string). 4 number cards (Total / Unique Admins / Last 24h / Last 7d); Last-24h + Last-7d bypass the saved-report's date filter so cards always reflect "right now". Donut shows top-5 actions + "Other" bucket (handles tenants with 50+ distinct action values). `description_excerpt` truncates at 200 chars (longer than other reports — the description IS the row's value). `created_at` + `created_time` split into separate projection columns. Diff / before-after capture deliberately out of scope. LOG-prefixed identifiers.
    • **Notification Delivery Report** — reads the `notifications` table written by the notification microservice (admin_backend is read-only here; new `App\Models\Notification` is fillable-empty). Channel / Status / Source filter keys prefixed `notif_*` (defensive naming from Promotion's `promo_status` rename — avoids clashing with the shared user-status dropdown which has incompatible value sets). `raw_body` + `variables` NEVER projected at any level — security boundary; the service uses explicit `select()` to keep them off the wire. 4 number cards (Total / Sent% / Failed% / Pending count); Pending is an absolute count not a percentage so trickle outages don't round to 0%. Failed% headline collapses `failed + bounced`; donut keeps them separate so the split is one glance away. Filters cover Channel (email/slack/in_app/sms/whatsapp), Status (sent/failed/bounced/pending), Recipient Type (user/admin/compliance), plus Template Slug / Source contains-search. Open / click tracking deliberately out of scope (no `opened_at` / `clicked_at` columns). Re-send action stays in the notification microservice's admin tools — report surfaces failures, remediation lives elsewhere. NFN-prefixed identifiers.
    • Tenant-scoped catalog cache on admin_backend — `MiscController::countries` and `MiscController::regions` wrapped in `Cache::remember()` with 15-minute TTL keyed `catalog:countries:{slug}` / `catalog:regions:{slug}`. Returns `->toArray()` so the cached payload is deterministic across MySQL/PostgreSQL drivers and database/redis cache stores. No invalidation hooks because no admin CRUD endpoints exist for these tables — TTL bounds staleness if a seeder runs.
    • Lazy-loaded Recent tables on admin_ui — only the default-active Client tab is server-rendered on initial page load; other tabs fetch their Recent list via `recent{Type}Json` endpoints on tab click. `AbortController` cancels stale fetches on rapid tab-switching. Pattern locked in for future reports.
    • Per-tab in-page cache + Refresh icon — each tab's Recent data is fetched once per page load and cached; clicking the same tab again does nothing. A Refresh button on each card header (5 total) forces a re-fetch with a spinning state and a 15s hard cap. Generate flow still navigates away to the new report's view, so returning to `/report` rehydrates from PHP anyway.
    • Real `last_login_at` column for users — needs migration + auth wiring.
    • Role-based visibility on saved reports — currently every admin with `reports.view` sees every saved report.
    • Scheduled recurring runs ("email me this report every Monday"), shareable internal report links, bulk delete / re-run — Phase 9 ideas.
    • MT5-backed reports (Trade History, Open Positions, Daily P&L, Volume & Commission, Margin & Equity, Symbol Usage) — Phase 8, blocked on MT5 integration decisions.
    • Sales Performance Report — UI tab stays "Coming soon" until requirements firm up.
  • 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.
  • Admin-side in-app notifications — bell widget, per-admin opt-in subscription preferences, and 18 admin-facing events (Milestone E) migration-service notification api_gateway admin_ui admin_backend profile_microservice account_microservice finance_microservice helpcenter auth_microservice
    Tenant DB migration required. Run php artisan tenant:migrate {slug} for every active tenant. Adds notification_subscriptions table (admin_id, event_slug, enabled). Without it the admin preferences endpoints fail with SQL errors and the fan-out dispatcher returns "no subscribers" for every event. Env vars required on auth_microservice (new caller). auth_microservice now talks to the notification microservice for the new_user_registered admin event. Set NOTIFICATION_SERVICE_URL, NOTIFICATION_SERVICE_TIMEOUT, NOTIFICATION_SERVICE_ENABLED, and INTERNAL_API_KEY on auth_microservice. INTERNAL_API_KEY must match the value on the notification microservice. No new template seeder. Admin in-app rows render through the existing generic_in_app template (already seeded in the user-side rollout) — Path D extended to the admin surface. Default subscriptions are OFF. Existing admins start with zero subscriptions and an empty bell. They must visit /admin/notification-preferences and opt in to the events they care about. This avoids paging every admin for every event on day one. Deployment order: migration-service → notification → api_gateway → admin_ui → admin_backend + profile_microservice + finance_microservice + account_microservice + helpcenter + auth_microservice. Cache clears: after deploy, run php artisan config:clear && php artisan route:clear on every service touched, plus php artisan view:clear on admin_ui.
    • The fixed `generic_in_app` template still renders every in-app row; the new fan-out dispatcher accepts an event slug + in_app block and stamps the slug on the row for audit / preference lookup
    • Per-admin subscriptions table answers "who is subscribed to this event slug?" at dispatch time. Direct-send events (welcome bell on invite, ticket assignment) bypass the table entirely
    • Self-notification silenced — the triggering admin (when known) is excluded from the fan-out so they don't see a bell for an action they just performed (Gmail/Slack convention)
    • New migration `2026_05_07_120001_create_notification_subscriptions_table` — composite unique on `(admin_id, event_slug)`, hot-path index on `(event_slug, enabled)` for the fan-out subscriber lookup
    • Thin `AdminRegister` model on the notification branch with `HasApiTokens` — exists only so Sanctum's polymorphic tokenable resolver can locate admin tokens minted by admin_backend; no auth logic
    • `RequireAdminToken` middleware + `admin.token` alias — rejects non-admin principals with 403 on admin-scoped endpoints (mirrors the `RejectReadonlyToken` pattern)
    • `NotificationSubscription` model + 18-event catalog in `config/notification.php` (six categories: compliance, finance, support, mt5, user_lifecycle, admin_lifecycle; each event flagged `subscribable: true|false`)
    • `AdminSelfNotificationController` exposes the admin's own bell feed under `auth:sanctum` + `admin.token`: list, unread-count, mark-read, mark-all-read — scoped to `recipient_type='admin'`
    • `AdminSubscriptionController` exposes preference CRUD: GET returns the catalog merged with the admin's stored opt-ins (drops `subscribable=false` events); PUT bulk-upserts with allow-list filtering against the catalog
    • `NotificationDispatcher::sendToAdmins()` queries subscribers, silences the triggering admin, and creates one `recipient_type='admin'` row per opted-in subscriber via the existing `dispatchSingle('in_app', ...)` path — same persistence contract as user-side
    • New internal endpoint `POST /api/send-to-admins` validates the event_slug against the catalog allow-list (subscribable=false slugs use the regular `/api/send` path); empty subscriber list returns 202 with `recipients=0` rather than failing
    • api_gateway proxy: 6 routes inside `$tenantRoutes` under `/admin/me/notifications/*` and `/admin/me/notification-preferences`, throttled at 300/min and forwarding the admin's bearer token (no `X-Internal-Key` — Sanctum authenticates the principal downstream)
    • Header bell replaces the static `9` notification button — live unread badge, dropdown showing the most recent 10 notifications, mark-all-read action, link to preferences page
    • Polling every 60s via `public/js/admin-notifications-bell.js`, pauses on `visibilitychange`, ESC / outside-click closes the dropdown
    • `/admin/notification-preferences` page renders one toggle row per subscribable event, grouped by category with a "N of M on" pill per category. Save button enables only when state differs from the server snapshot; PUT submits all subscriptions in one call; `beforeunload` warns on unsaved changes
    • All bell + preferences class names avoid the `loading|spinner|preloader` substrings that admin_ui's global CSS force-hides (gotcha #2)
    • `profile_microservice` (5 events, 7 call sites): `doc_uploaded_identity` (uploadIdentityDocument — front/back and single-file paths), `doc_uploaded_address`, `doc_uploaded_networth`, `doc_uploaded_signature`, and `payout_destination_added` (createUserBankAccount + createUserCryptoAddress share one slug — admins watching for fraud see both kinds in one feed)
    • `finance_microservice` (6 events): `new_withdrawal_submitted_bank`, `new_withdrawal_submitted_crypto`, `new_deposit_submitted` (manual-flow path), `deposit_receipt_uploaded`, `deposit_received_psp` (Match2Pay webhook auto-approve), `new_internal_transfer_submitted`
    • `helpcenter` (1 event): `new_ticket_created`
    • `account_microservice` (1 event): `new_mt5_account_request` (admin-review branch only — instant-provisioning path doesn't fan out)
    • `auth_microservice` (1 event, **new NotificationService scaffold**): `new_user_registered` — first time auth_microservice talks to the notification microservice; `notification_service` block added to `config/services.php`
    • `admin_backend` (4 events): `admin_employee_invited` (direct send — welcome bell waits in the new admin's feed until they verify and log in), `admin_employee_email_verified` (subscriptions, verifier silenced), `ticket_assigned_to_me` (direct send — assignee resolved from the `assign_now` name string against AdminRegister, silently skipped if no name match), `ticket_internal_comment` (subscriptions, commenter silenced)
    • Subscriptions default OFF — opt-in only. Fresh admins see an empty bell until they visit preferences. Avoids paging every admin for every event on day one and lets the team self-select into the noise they actually want
    • All 18 call sites wrapped in try/catch — notification fan-out failures never block the business action (registration, withdrawal, KYC upload, ticket assignment, etc.)
    • Self-notification silenced via `triggering_admin_id` on subscription-based events — admins don't get bells for their own actions
    • Direct-send events (`admin_employee_invited`, `ticket_assigned_to_me`) bypass the subscription table entirely so the invitee / assignee always sees the row regardless of their preferences (these aren't optional pages to receive)
    • Two events live in the catalog as non-subscribable (the direct-send ones) — they appear in `config/notification.php` for completeness but the preferences endpoint filters them out so admins don't see meaningless toggles
    • admin_ui CSS scrupulously avoids the `loading|spinner|preloader` substrings — every bell / preferences class uses alternatives like `is-fetching`, `admin-bell-busy` (gotcha #2)
    • Retention cron — read in-app rows older than 30 days could be purged daily but the cron is deliberately not part of this release; will land separately (1-day estimated effort, single artisan command + scheduler entry on the notification branch, covers user + admin rows in one job)
    • Role-based default suggestions — once the `admin_register.role` column gets seeded role values (`compliance`, `finance`, `support`, `mt5_admin`), offer a "subscribe me to recommended defaults for my role" button on the preferences page
    • Per-category digest mode — "deliver Finance events as one daily summary instead of real-time" — requires batching + scheduled send
    • Quiet hours — per-admin "don't notify me outside working hours"
    • Event de-duplication — if 50 deposits land in 5 minutes each subscribed admin gets 50 bells; accept the noise initially, add batching if the team complains
    • Multi-channel admin notifications — admin events are in-app only today; the fan-out dispatcher already supports `channels: ['in_app', 'email']`, just needs email template slugs authored
    • RBS `notify_admin_inapp` action — mirrors the deferred `notify_user_inapp` follow-up for when the RBS microservice goes live
  • Employee bulk actions — activate, deactivate, assign department, and delete multiple employees at once from the All Employees table; granular permission keys employees.bulk.status, employees.bulk.role, employees.bulk.delete admin_ui api_gateway admin_backend migration-service
  • Permission keys for employee bulk actions — employees.bulk.status, employees.bulk.role, employees.bulk.delete added to PermissionsSeeder and EmployeePermissionSeeder migration-service
  • Employee ID document upload — front/back image upload, storage, view & download with id_front_image / id_back_image columns on admin_register admin_ui api_gateway admin_backend migration-service
  • admin resend email action (max 4 sends)(admin_ui, apigateway and microservices/notifications)
  • Added analytics page admin_ui apigateway and admin_backend
Changed
  • Permission staleness check now reads from employee_permissions.updated_at (cached 10 min + invalidated on write) instead of scanning admin_logs, and admin_ui throttles checks to once per 30s per sessionStorage key admin_backend admin_ui api_gateway
  • DataTables row action menus — float above scroll containers, open one at a time with smooth transition (All Employees, Individual Clients, All/Live/Demo/Wallet Accounts) admin_ui
  • MT Settings tabs — defer Demo Groups, Bonus, Leverage, and MT5 list/settings fetches until their tab is first activated (no upfront load on inactive tabs) admin_ui
  • Direct Mail::to(...)->send(...) paths on six microservices now read SendGrid credentials from the tenant DB (notification_settings.sendgrid_api_key) instead of each container's .env MAIL_PASSWORD auth_microservice profile_microservice finance_microservice account_microservice admin_backend helpcenter
    No migration, no seeder. Existing NotificationSettingSeeder rows (already seeded for dev, demo, zyz, okm, and any future tenant via Phase 3 of TENANT_ONBOARDING_HSW.md) are the new source of truth. Verify notification_settings.sendgrid_api_key is populated on every active tenant before deploy. Mail is disabled (silent no-op) for any tenant without a sendgrid_api_key. The middleware switches mail.default to 'array' so Mail::to(...)->send(...) returns successfully without delivering. A warning is logged (Tenant has no sendgrid_api_key — skipping email send). Registration / password reset / OTP / etc. complete normally; only the email side-effect is suppressed. APP_KEY must be identical across the six affected microservices. The SendGrid key is encrypted at rest in notification_settings; mismatched keys cause DecryptException per-request, which the middleware logs and skips. Already enforced on the platform — flagging for completeness. Cache invalidation. Per-tenant config is cached for 1 hour under mail_config:{slug}. If you rotate a SendGrid key via SQL/seeder, run php artisan cache:clear (or cache:forget mail_config:{slug} if available) on each of the six services for instant effect. Otherwise the new key picks up within an hour. Env MAIL_PASSWORD becomes a dead value. Once the deploy is verified, MAIL_PASSWORD in each microservice's .env is no longer read by the mail path. Safe to delete after a week of monitoring; leaving it in place is harmless.
    • Per-request mail config override appended to each service's `ResolveTenant` middleware. Runs after the tenant DB connection is set up, reads `notification_settings` for `sendgrid_api_key`, `from_email`, `from_name`, decrypts via shared `APP_KEY`, rewrites `mail.mailers.smtp.*` and `mail.from.*` in `config()`, and forgets cached mailer instances so the next `Mail::to(...)` picks up the new credentials.
    • profile_microservice's `ResolveTenant` doesn't switch the default DB connection, so its variant builds the tenant connection on cache miss (via `TenantConnectionService`) before reading `notification_settings` via the named connection. Other five services use `DB::table(...)` directly since their `ResolveTenant` already switched the default.
    • 1-hour cache TTL keyed `mail_config:{slug}`. On Redis (live) the cache is shared across services so a warm cache benefits all six. On file/database cache (dev) each service warms its own cache independently.
    • All failures (DB unreachable, decryption error, cache backend down) are caught, logged at warning level, and the request continues with whatever mail config was in place. Middleware never breaks the request.
    • `auth_microservice` — VerifyEmail, ResetPassword, DemoVerify (4 call sites in RegisterController)
    • `profile_microservice` — VerifyEmail, ResetPassword, AccountCreated (4 call sites)
    • `finance_microservice` — DepositRequestSubmitted, TransferSuccess, OtpMail (withdrawal OTP), WithdrawalConfirmation x2 (5 call sites)
    • `account_microservice` — AccountCreated x2, MTAccountPasswordChanged, VerifyEmail, ResetPassword (5 call sites)
    • `admin_backend` — AccountCreated x3, EmployeeVerifyMail, EmployeeCredentialsMail, OtpMail, ForgotPasswordMail, ComplianceDocumentStatusMail, KycStatusMail, WithdrawalApproved, WithdrawalDisapproved (highest volume, ~12 call sites across 6 controllers)
    • `helpcenter` — TicketCreated (1 call site)
    • `notification microservice` — already reads `notification_settings.sendgrid_api_key` directly via its `EmailChannel`, which uses the SendGrid HTTP API (not SMTP). This release doesn't touch that path.
    • `api_gateway`, `ui_gateway`, `admin_ui`, `rbs`, `migration-service` — don't send mail.
    • Migrating call sites from `Mail::to(...)->send(...)` to `NotificationService::send(['channels' => ['email'], ...])` so the notification microservice's email log captures every send. Tracked separately as the "retire direct-mail paths" follow-up; this release unifies credentials only.
    • Per-event email template authoring on migration-service (prerequisite for the above).
    • Per-tenant from-address override at the Mailable level — currently uses tenant's `notification_settings.from_email` if set, else falls through to the existing `MAIL_FROM_ADDRESS` env on each service.
v1.1.2.1 2026-05-12
Fixed
  • 500 errors across all public sites after server kernel-update reboot — caused by Redis silently skipping bind to 172.18.0.1 because redis-server.service started before docker.service on boot api_gateway
Infrastructure
  • Restored iptables rules for Redis (6379) and MariaDB (3306) wiped during reboot — both ports now restricted to Docker subnet 172.18.0.0/16 + localhost api_gateway
  • Removed leftover permissive ACCEPT 0.0.0.0/0 rule on port 3306 that was leaving MariaDB exposed publicly api_gateway
  • Added systemd drop-in for redis-server.service (After=docker.service, Requires=docker.service) so Redis binds to the Docker bridge gateway on every boot api_gateway
v1.1.2.0 2026-04-17
Changed
  • Email templates now use dynamic tenant company branding instead of hardcoded values admin_backend auth_microservice account_microservice finance_microservice profile_microservice helpcenter
  • Legacy templates: replaced config('app.name') and inline Setting::where() queries with $companyName passed from controller via Mailable
  • New (OKM) templates: replaced all hardcoded Oktrum branding — company name, logo, website, contact email, phone, addresses — with dynamic profile.company.* tenant settings
  • Added companyProfile() bulk-fetch method to EmailTemplateFlag on admin_backend
  • Each microservice Mailable now resolves the full company profile in one query
Added
  • New settings seeder CompanyProfileSettingSeeder with 5 keys: profile.company.logo_url, profile.company.website_url, profile.company.phone, profile.company.address_physical, profile.company.address_registered migration-service
  • Seeder rules in database-conventions.md SOP — use firstOrCreate for tenant-customisable settings, never modify existing seeders api_gateway
  • Updated versioning SOP to 4-segment format: MAJOR.FEATURE.IMPROVEMENT.FIX api_gateway
  • Added IDWise and ARP gateway (admin_ui, ui_gateway, apigateway, microsevices/profile, microservices/finance, microservices/migration and admin_backend)
  • Added internal transfer limits with limits.transfer_min key rename and implement on ui_gateway and also fix toast in admin (admin_ui, ui_gateway, microservices/finance, microservices/migrations and admin_backend)
v1.1.1 2026-04-09
Changed
  • Hide manual "Add Payment Method" option and the model code field from the admin UI admin_ui
  • diplayed logo in uigateway where was used (mean everyweher ) ui_gateway, apigatway and microservices/profile
  • add system.NXRA setting key in SettingSeeder for conditional site design variant (microservices/migrations)
Fixed
  • Compliance redirect now sends users to the Upload Documents page when their profile is complete but documents were rejected, instead of always redirecting to Profile ui_gateway
v1.1.0 2026-04-08
Added
  • Permanent (hard) delete option for clients admin_ui api_gateway admin_backend
    • Toggle switch in soft-delete confirmation modal
    • "Permanently Delete" action for already soft-deleted clients
    • "Permanently Delete Selected" bulk action
    • Deletes all related data: profile, accounts, transactions, invoices, wallets, documents, bank accounts, tickets
    • Admin "Access Client" readonly impersonation (migration-service, auth_microservice, api_gateway, ui_gateway, admin_ui, finance_microservice, account_microservice, profile_microservice, helpcenter)
    • 15-minute Sanctum token with `readonly` ability
    • `RejectReadonlyToken` middleware on all microservices blocks writes from readonly tokens
    • `ReadonlyGuard` middleware on ui_gateway blocks non-GET requests in readonly session
    • Red readonly banner and global JS to hide submit buttons
    • `frontend_url` column on `tenants` table for multi-tenant dashboard URL resolution
    • New admin setting to restrict Live accounts for unapproved users: when enabled, users who have not completed compliance can still access the Accounts page but can only view and create Demo accounts. The Live account section is hidden until compliance is approved.
    • Added new payment gateway for nxra (EPG) (ui_gateway,microservices/finance, microservices/migrations,apigateway,admin_ui, admin_backend)
    • Added new Archive option in admin CRM (admin_ui, admin_backend and microservices/migrations)
    • Added new sales and leads sections in admin CRM (admin_ui)
    • Added disclaimer page view option for all (admin_ui, admin_backend, apigateway and microservices)
    • Added promotions page and gave delete and edit option in client info (admin_ui, admin_backend, apigateway)
Fixed
  • API Gateway client delete proxy now forwards request body to downstream service api_gateway
  • Fixed deposit table detail popup not closing automatically after a successful action
  • Live-account, payment-method & promotion modals now open as top-right drawers with a white close icon in the non-NXRA theme; Trade History toolbar styled for rest code (ui_gateway).
  • Replace sidebar icons with NXRA PNGs (Home, Accounts, Finance, Market,
  • Sidebar text set to #FFFFFF; active tab icon turns black on the gold bg.(ui_gateway)"
v1.0.0 2026-03-25
Released
  • Initial production deployment — OKM tenant live on servers.com Bahrain
  • 11 microservices + 3 queue workers deployed via Docker Compose
  • MariaDB 10.6 on app server, host Redis, Nginx → Apache → Docker proxy chain
  • SSL via Let's Encrypt on all 3 public domains (my.oktrum.com, bo.oktrum.com, okm.api.ataria.pro)
  • Gateway DB (ataria_gateway) + tenant DB (okm) with unified APP_KEY
  • Shared documents volume, iptables firewall rules, localhost-only port bindings