Files
baya-monorepo/docs/flows/admin-backoffice.md
T
2026-08-02 17:18:36 +03:30

15 KiB
Raw Blame History

Flow — admin-backoffice

Last verified: 2026-08-02 against commit c841bde

Actor(s): admin (super_admin · admin · support · finance · moderation) · Status: mocked Client: partial (19 of 21 consoles on mocked seams, 2 on real) · Server: partial (real handlers, 403 for every seeded admin) Business source: product/business/14-notifications-and-admin.md Integration: docs/integration/domains/admin.md

What it does

The single operator console behind Balinyaar: approve nurses, moderate reviews, run the weekly payout batch, issue refunds, triage support alerts and tickets, edit the platform's runtime rates, and read the append-only audit trail. It is the only surface where a human authorises money movement — a scheduled job may generate a draft payout batch, but process is always an explicit admin action.

⚠ Read this first — the console cannot reach the server

Every [Authorize(ConstantPolicies.DynamicPermission)] endpoint returns 403 for both seeded admin accounts (09120000020 super_admin, 09120000021 finance). 23 controllers carry that policy. The chain:

Link file:line What it does
the gate DynamicPermissionService.cs:9 if (user.IsInRole("admin")) return true; — the literal string, not "any admin sub-role"
the fallback same file :15-19 else requires a DynamicPermission claim whose value is exactly "{area}:{controller}:"
the vocabulary RoleNames.cs:13-17 Admin · Support · Finance · Moderation · SuperAdmin are five sibling roles — super_admin is not a superset of admin
the seeder DemoWorldDefinitions.cs:144-145 grants RoleNames.SuperAdmin / RoleNames.Financenever RoleNames.Admin
the missing half SeedDataBase.cs:50-71 the only code path that calls AddToRoleAsync(user, "admin"), and it returns early unless Seed:AdminUsername and Seed:AdminPassword are configured. Neither is in appsettings.Development.json

No code anywhere writes a DynamicPermission claim, so branch 2 can never fire either. No seeded account satisfies either branch.

Why this is invisible in the UI: USE_ADMIN_MOCK = true (admin/constants.ts:9), plus verification, payouts, refunds and partnerCenter are all mock-primary — so 19 of the 21 consoles render a complete, filterable, mutable world out of in-browser fixtures and never call the API at all. The console looks built.

Live-probed 2026-08-02 (super_admin token unless noted):

Probe Result
GET /api/v1/me 200 · {"id":6,"roles":["super_admin"]} — the token is fine
GET /api/v1/tickets?page=1 (user scope, same token) 200, 9 tickets — non-admin endpoints work normally
GET /api/v1/admin/tickets?page=1 403 {"isSuccess":false,"statusCode":403,"message":"Authorization Error"}
GET /api/v1/admin/reviews/moderation_queue?page=1 403
POST /api/v1/platform_config/update_platform_config (valid body) 403 — writes are blocked at authz, before validation. Nothing mutates
GET /api/v1/admin_payouts/batches (finance token) 403

Screens

21 routes, all inside the 480 px phone frame. RoleGuard expected=admin admits any of the five codes; useAdminCapabilities() (hooks/capabilities.ts:43) then hides group tabs and rows per code — a UI hint only, the consoles stay URL-reachable.

Group Route Client seam Notes
/fa/admin none Overview hub; NavHubList filtered by capabilities
اعتماد /fa/admin/trust none Group root
/fa/admin/verification · /…/[nurseId] verification MOCK Queue folded per-nurse; case view with DocumentViewer re-signing its own URL
/fa/admin/reviews reviews REAL 4 moderation tabs → 403s live
مالی /fa/admin/finance none Group root
/fa/admin/payouts · /…/[batchId] payouts MOCK Dry-run preview → idempotency-keyed run; gross clawback = net
پشتیبانی /fa/admin/support none Group root
/fa/admin/tickets · /…/[id] tickets REAL (+ refunds MOCK via RefundPanel) Internal-notes composer → 403s live
/fa/admin/alerts admin MOCK support_alerts triage; assign-to-self, resolve with note
سیستم /fa/admin/system auth Always shown — carries sign-out
/fa/admin/config admin MOCK Typed editor per data_type; rate keys validated to [0, 1)
/fa/admin/holidays admin MOCK iranian_holidays + the is_bank_closed flag
/fa/admin/audit admin MOCK Read-only; rows expand to the changed_fields diff
/fa/admin/partners · /…/[id] admin + partnerCenter MOCK IBAN write-then-masked, never displayed
/fa/admin/users admin MOCK Backed by admin_users/searchroute does not exist
/fa/admin/roles admin MOCK RBAC grid; in-page banner says it is deferred
/fa/admin/notifications none PlaceholderScreen stub; true orphan, nothing links to it

API

Shapes belong to the integration tree — do not restate them here.

Console Endpoints Contract
config · holidays · audit · alerts platform_config/*, holidays/*, audit/get_audit_trail, support_alerts/* admin.md
verification queue + case admin_verifications/* verification.md
payout batches admin_payouts/* payouts.md
refunds (in the ticket thread) admin_refunds/* refunds.md
review moderation admin/reviews/moderation_queue reviews.md
ticket queue + admin thread admin/tickets, admin/tickets/{id} tickets.md
partner centers admin/partner-centers* partner-center.md
roles · user directory admin_roles/*, admin_users/* phantom — not on the wire (REQ-031 deferred; REQ-061 never filed)

Client chain, verified link by link for the config console: admin/config/page.tsxusePlatformConfigs() (services/admin/hooks/usePlatformConfigs.ts) → adminApi (apis/index.ts:10, the one-line selector, currently resolving to adminMockApi) → adminClientApi.listPlatformConfigs (apis/clientApi.ts:75) → clientFetchGET /api/v1/platform_config/get_platform_configsPlatformConfigControllerListPlatformConfigs handler. Every link exists. Only the selector and the authz gate stand in the way.

Rules that must hold

Rule Value Source of truth Product
Money movement stays human-approved the scheduler opens a draft batch; process is an explicit admin action WeeklyPayoutGenerationJob + ExecutePayoutBatch 10-payouts.md
Config is rows read at compute time, and a rate change is never retroactive rates snapshotted onto the row (Bookings.PlatformFeeRate, Invoices.VatRate) CONFIG via IPlatformConfig 14-…-admin.md
Commission / VAT platform_fee_rate = 0.15, vat_rate = 0.10, VAT on the commission line only CONFIG keys (0.15 is a seeded default, not a product mandate) 13-tax-invoicing-and-legal.md
Rate keys are in [0, 1) console validates before writing RATE_CONFIG_KEYS, admin/constants.ts:27
Holidays shift the payout date server resolves the next business day; the client never computes a shift IHolidayCalendar ROWS 10-payouts.md
Audit trail is append-only no edit/delete affordance; retention 730 d general / 2555 d financial CONFIG audit_retention_*_days
is_internal never leaves the query layer admin thread is the only surface that carries internal notes TicketRepository projections 12-messaging-and-emergencies.md
Refunds are admin-only and ticket-anchored no customer self-service; every refund splits across both fee legs AdminRefundsController, CK_Refunds_LegSplit 07-cancellation-and-refunds.md
Low-rating alert threshold ≤ 2 CONFIG min_rating_for_support_alert 11-reviews-trust-and-safety.md
Publishing a review recomputes the nurse aggregate server-side on every status transition review moderation handlers 11-…-safety.md
Suspending a nurse flips is_searchable = 0 on every one of her rows rows are kept, never deleted SearchIndexMaintainer.cs:177,248 11-…-safety.md

How to test

  1. Log in as 09120000020 (نگار مدیری, super_admin) — see testing-setup.md. Expect: RoleGuard admits you and the app lands on /fa/admin with all four group tabs visible.
  2. Walk /fa/admin/config/fa/admin/holidays/fa/admin/audit/fa/admin/alerts. Expect: each renders a populated, filterable console; edits appear to save. This proves nothing about the server — these are adminMockApi fixtures held in module state, and they reset on every page reload or HMR. Open devtools Network: there is no request.
  3. Open /fa/admin/tickets and /fa/admin/reviews — the two consoles on real seams. Expect: an error state, not a queue. clientFetch toasts the 403 itself.
  4. Log in as 09120000021 (کامران مالی, finance) and open /fa/admin. Expect: «اعتماد» and «پشتیبانی» tabs are gone; only «مالی» and «سیستم» remain. /fa/admin/payouts renders a mock batch list. /fa/admin/audit is hidden (audit is admin/super_admin only) but still URL-reachable.
  5. Confirm the gap directly, without the browser: curl -s --noproxy '*' "http://localhost:5002/api/v1/platform_config/get_platform_configs?page=1" -H "Authorization: Bearer $T_09120000020" Expect: 403 {"isSuccess":false,"statusCode":403,"message":"Authorization Error"}, while GET /api/v1/me with the same token returns 200.

Break-glass (UNVERIFIED — not executed for this stamp): add "Seed": { "AdminUsername": "…", "AdminPassword": "…" } to appsettings.Development.json before boot. SeedDataBase.SeedBootstrapAdminAsync then mints a user in the literal admin role, which satisfies branch 1. That account is username/password, not phone-OTP, so it cannot log in through the web UI — drive the API directly. It was not tested here because it needs a server restart shared with other agents.

Seeded-world caveat: the demo world is 7 days stale (see testing-setup.md). Even with the RBAC gap fixed, the verification queue has no in_review step awaiting a decision beyond nurse 3's two blocking steps, and every dispute window has closed — so a live payout preview would sweep bookings you did not stage.

Known gaps

  • DynamicPermissionService.CanAccess grants only on the literal role "admin". The four sibling admin roles — super_admin, support, finance, moderation — get 403 on all 23 DynamicPermission controllers. DynamicPermissionService.cs:9. A super_admin has less access than an admin.
  • No seeded account holds the literal admin role. DemoWorldDefinitions.cs:144-145 grants super_admin/finance; SeedDataBase.cs:55-56 returns early because Seed:AdminUsername/AdminPassword are unset. The entire backoffice is untestable end-to-end out of the box.
  • No code path ever writes a DynamicPermission claim, so the per-controller fallback branch (DynamicPermissionService.cs:15-19) is dead — the claim key "{area}:{controller}:" has no producer.
  • The 403 is invisible in the UI. USE_ADMIN_MOCK = true plus mocked verification/payouts/refunds/ partnerCenter means 19 of 21 consoles never call the API. A reviewer clicking through the console concludes it works.
  • /fa/admin/tickets and /fa/admin/reviews are broken for the operator right now — real client seams onto 403ing controllers. The only two consoles where the defect surfaces.
  • admin/apis/clientApi.ts pageQuery() sends page_size; the controllers declare PageSize. Model binding is case-insensitive, not separator-insensitive, so every admin list silently falls back to the server default page size the moment USE_ADMIN_MOCK flips.
  • /fa/admin/roles has no serveradmin_roles/list_roles|grant_role|revoke_role are phantom (REQ-031 deferred). Admin roles are seeded, never managed.
  • /fa/admin/users has no serveradmin_users/search|lookup are phantom, and REQ-061 was never filed in the ledger despite ten client files citing it. AuditLogRow shows #id instead of a name.
  • /fa/admin/notifications is a PlaceholderScreen stub and a true orphan — no link reaches it, and AdminLayout renders no NotificationBell.
  • POST /api/v1/holidays/delete_holiday is unwired — the console offers no delete affordance.
  • admin_cancellation_policies/list|upsert are unwired — no screen edits the cancellation tiers, so the seeded standard_24h/standard_inside_24h rows are effectively read-only in production.
  • admin_search/rebuild_index and admin_booking_requests/expire have no UI — ops one-shots reachable only by curl (and both 403 for a seeded admin).
  • useAdminCapabilities hides tabs but does not block routes. Every console stays URL-reachable for any admin code; the enforcement is expected to be server-side, which is currently a blanket 403.