const EDR_FREIGHT_APP_KEY = "edr_freight_app"; export type FreightPermissionSeed = { id: string; key: string; name: { am: string; en: string }; applicationKey: string; }; export const RULE_ENGINE_RESOURCE_SLUGS = [ "cargo-types", "container-types", "wagon-types", "service-types", "yards", "shipping-lines", "weight-limit-rules", "priority-configs", "rates", "approval-rules", "yard-distances", // Keep new slugs at the END: ruleEngineCrudId derives ids from list index, // so a mid-list insert would shift ids already seeded for later slugs. "truck-types", "transit-agents", "operations-targets", ] as const; export type RuleEngineResourceSlug = (typeof RULE_ENGINE_RESOURCE_SLUGS)[number]; const slugToResourceKey = (slug: RuleEngineResourceSlug): string => slug.replace(/-/g, "_"); const VIEW_KEY_SUFFIX = ":view"; const READ_KEY_SUFFIX = ":read"; /** The `:read` twin of a `:view` key, or null if `key` is not a view key. */ export const readTwinOf = (key: string): string | null => key.endsWith(VIEW_KEY_SUFFIX) ? `${key.slice(0, -VIEW_KEY_SUFFIX.length)}${READ_KEY_SUFFIX}` : null; const perm = (id: string, key: string, en: string): FreightPermissionSeed => ({ id, key, name: { am: en, en }, applicationKey: EDR_FREIGHT_APP_KEY, }); /** * Every report key ever seeded, in seed order. * * NEVER reorder or delete an entry: reportPermId derives a permission's uuid * from its index here, so a shift would re-map ids already granted to roles. * Retiring a report means adding it to RETIRED_REPORT_KEYS, not removing it. * New keys go at the END. */ const SEEDED_REPORT_KEYS = [ "bookings-list", "revenue-by-customer", "aging-receivables", "contract-utilization", "wagon-fleet-status", "wagon-status-duration", "wagon-requests", "locomotive-fleet-status", "booking-status-breakdown", "train-schedule-status", "train-turnaround", "wagon-teu-utilization", "loaded-capacity", "global-logistics-wagons", "customer-status", "contract-lifecycle", "customs-documents", "invoicing-pipeline", "first-last-mile-bookings", "invoices-by-status", "payments-by-status", "revenue-summary", "cargo-summary", "revenue-by-category", "revenue-transactions", "revenue-by-period", "revenue-by-route", "revenue-top-customers", "payment-classification", "revenue-reconciliation", "receivables-payables", "revenue-anomalies", "station-staying-time", "turnaround-cycle", "train-delays", "trainset-performance", "teu-performance", "cargo-volume-performance", "charged-vs-actual-volume", "cargo-volume-by-station", "port-warehouse-summary", "loading-unloading", ] as const; /** * Reports whose definition was deleted (see modules/reports/definitions) — a * flat list the Exports module and its backoffice table already serve, or a * narrower view of a report that supersedes it. Their permissions stay seeded * so no live report's uuid moves; nothing resolves them to a definition. */ const RETIRED_REPORT_KEYS = [ "bookings-list", "customer-status", "contract-lifecycle", "invoices-by-status", "payments-by-status", "revenue-summary", ] as const; export type ReportKey = Exclude< (typeof SEEDED_REPORT_KEYS)[number], (typeof RETIRED_REPORT_KEYS)[number] >; /** One entry per live report definition — what the catalog and presets use. */ export const REPORT_KEYS: readonly ReportKey[] = SEEDED_REPORT_KEYS.filter( (k): k is ReportKey => !(RETIRED_REPORT_KEYS as readonly string[]).includes(k), ); export const reportPermissionKey = (key: string): string => `edr_freight_app:reports:${key.replace(/-/g, "_")}:view`; const reportPermId = (index: number): string => `a4f00002-0001-4000-8000-${(index + 1).toString(16).padStart(12, "0")}`; const titleCase = (slug: string): string => slug .split("-") .map((w) => w[0].toUpperCase() + w.slice(1)) .join(" "); // Seeded from SEEDED_REPORT_KEYS, not REPORT_KEYS: a retired report keeps its // index and its permission row, which is what stops the live ids from moving. export const REPORT_PERMISSIONS: FreightPermissionSeed[] = SEEDED_REPORT_KEYS.map((key, index) => perm( reportPermId(index), reportPermissionKey(key), `Report: ${titleCase(key)}`, ), ); /** * Overview dashboard layouts (see the backoffice's role-dashboards.config.ts, * where `LAYOUTS` renders one composition per key). Unlike reports, a caller * lands on exactly ONE layout, so `OVERVIEW_LAYOUT_KEYS` is also the priority * order: whoever resolves the permission set picks the FIRST key here the * caller holds — the specific operational view wins over the broad executive * one, same rule the old role/position-key table encoded. * * NEVER reorder — GET /overview/layouts and the frontend both walk this array * to break ties, so reordering silently changes who gets which dashboard. */ export const OVERVIEW_LAYOUT_KEYS = [ "clearance", "occ", "operation", "marketer", "finance", "executive", ] as const; export type OverviewLayoutKey = (typeof OVERVIEW_LAYOUT_KEYS)[number]; export const OVERVIEW_LAYOUT_LABELS: Record = { clearance: "Clearance & logistics dashboard", occ: "Control centre dashboard", operation: "Operations dashboard", marketer: "Marketing dashboard", finance: "Finance dashboard", executive: "Executive dashboard", }; export const overviewLayoutPermissionKey = (key: string): string => `edr_freight_app:overview:${key}:view`; const overviewLayoutPermId = (index: number): string => `a4f00003-0001-4000-8000-${(index + 1).toString(16).padStart(12, "0")}`; export const OVERVIEW_LAYOUT_PERMISSIONS: FreightPermissionSeed[] = OVERVIEW_LAYOUT_KEYS.map((key, index) => perm( overviewLayoutPermId(index), overviewLayoutPermissionKey(key), `Overview layout: ${OVERVIEW_LAYOUT_LABELS[key]}`, ), ); export const BOOKING_PERMISSIONS: FreightPermissionSeed[] = [ perm( "a1000001-0001-4000-8000-000000000001", "edr_freight_app:bookings:view", "View bookings", ), perm( "a1000001-0001-4000-8000-000000000002", "edr_freight_app:bookings:staff_accept", "Accept booking intake", ), perm( "a1000001-0001-4000-8000-000000000003", "edr_freight_app:bookings:request_changes", "Request booking changes", ), perm( "a1000001-0001-4000-8000-000000000004", "edr_freight_app:bookings:reject", "Reject booking submission", ), perm( "a1000001-0001-4000-8000-000000000005", "edr_freight_app:bookings:approve_line_staff", "Approve as line staff", ), perm( "a1000001-0001-4000-8000-000000000006", "edr_freight_app:bookings:approve_director", "Approve as director", ), perm( "a1000001-0001-4000-8000-000000000007", "edr_freight_app:bookings:approve_ceo", "Approve as CEO", ), perm( "a1000001-0001-4000-8000-000000000008", "edr_freight_app:bookings:reject_approval", "Reject at approval step", ), perm( "a1000001-0001-4000-8000-000000000009", "edr_freight_app:bookings:generate_contract", "Generate contract", ), perm( "a1000001-0001-4000-8000-00000000000a", "edr_freight_app:bookings:sign_staff", "Staff contract signature", ), perm( "a1000001-0001-4000-8000-00000000000d", "edr_freight_app:bookings:operations", "Booking operations", ), perm( "a1000001-0001-4000-8000-00000000000e", "edr_freight_app:bookings:cancel", "Cancel booking", ), perm( "a1000001-0001-4000-8000-000000000023", "edr_freight_app:bookings:clearance_view", "View customs-clearance queue", ), perm( "a1000001-0001-4000-8000-000000000020", "edr_freight_app:bookings:review_documents", "Review clearance documents", ), perm( "a1000001-0001-4000-8000-000000000021", "edr_freight_app:bookings:upload_clearance_output", "Upload customs output documents", ), perm( "a1000001-0001-4000-8000-000000000022", "edr_freight_app:bookings:finalize_clearance", "Finalize document clearance", ), perm( "a1000001-0001-4000-8000-00000000000f", "edr_freight_app:train_scheduling:view", "View train scheduling", ), perm( "a1000001-0001-4000-8000-000000000011", "edr_freight_app:fleet:view", "View fleet", ), perm( "a1000001-0001-4000-8000-000000000012", "edr_freight_app:fleet:manage", "Manage fleet", ), perm( "a1000001-0001-4000-8000-000000000013", "edr_freight_app:admin", "Freight administration", ), perm( "a1000001-0001-4000-8000-000000000024", "edr_freight_app:bookings:create", "Create booking", ), // Header alarm for the document-review deadline: its own key so only the // position types that actually decide operation requests are alerted. perm( "a1000001-0001-4000-8000-000000000025", "edr_freight_app:bookings:doc_review_alert", "See document-review deadline alarm", ), // Partial wagon cancellation (paid bookings): staff-side keys. The customer // portal needs none — customer actions are ownership-scoped on the API. perm( "a1000001-0001-4000-8000-000000000026", "edr_freight_app:bookings:wagon_cancellation_view", "View wagon cancellation history", ), perm( "a1000001-0001-4000-8000-000000000027", "edr_freight_app:bookings:wagon_cancellation_void", "Void a pending wagon cancellation", ), perm( "a1000001-0001-4000-8000-000000000028", "edr_freight_app:bookings:wagon_cancellation_rebook", "Rebook cancelled wagons for a customer", ), // Shared-wagon gate: two customers' cargo on one wagon is a commercial call, // so it is signed off separately from the ordinary booking approvals — and // never by the GL user who created the pairing. perm( "a1000001-0001-4000-8000-000000000029", "edr_freight_app:bookings:approve_consolidation", "Approve shared-wagon consolidation", ), ]; /** * Contract-phase permissions (contract–booking separation). Mirror the booking * approval/sign/clearance permissions but scoped to the new contracts module. */ export const CONTRACT_PERMISSIONS: FreightPermissionSeed[] = [ perm( "a3000001-0001-4000-8000-000000000001", "edr_freight_app:contracts:view", "View contracts", ), // Intake actions are split per freight type (bulk vs container) — fresh ids // because the seeder upserts ON CONFLICT (key); reusing the old ids with new // keys would PK-collide with the legacy staff_accept/request_changes/reject rows. perm( "a3000001-0001-4000-8000-000000000011", "edr_freight_app:contracts:staff_accept:bulk", "Accept bulk contract intake", ), perm( "a3000001-0001-4000-8000-000000000012", "edr_freight_app:contracts:staff_accept:container", "Accept container contract intake", ), perm( "a3000001-0001-4000-8000-000000000013", "edr_freight_app:contracts:request_changes:bulk", "Request bulk contract changes", ), perm( "a3000001-0001-4000-8000-000000000014", "edr_freight_app:contracts:request_changes:container", "Request container contract changes", ), perm( "a3000001-0001-4000-8000-000000000015", "edr_freight_app:contracts:reject:bulk", "Reject bulk contract", ), perm( "a3000001-0001-4000-8000-000000000016", "edr_freight_app:contracts:reject:container", "Reject container contract", ), perm( "a3000001-0001-4000-8000-000000000005", "edr_freight_app:contracts:approve_line_staff", "Approve contract as line staff", ), perm( "a3000001-0001-4000-8000-000000000006", "edr_freight_app:contracts:approve_director", "Approve contract as director", ), perm( "a3000001-0001-4000-8000-000000000007", "edr_freight_app:contracts:approve_ceo", "Approve contract as CEO", ), perm( "a3000001-0001-4000-8000-000000000008", "edr_freight_app:contracts:generate_contract", "Generate contract document", ), // Staff counter-signature is split per freight type too — fresh ids for the // same reason as the intake keys above. perm( "a3000001-0001-4000-8000-000000000017", "edr_freight_app:contracts:sign_staff:bulk", "Staff contract signature: bulk", ), perm( "a3000001-0001-4000-8000-000000000018", "edr_freight_app:contracts:sign_staff:container", "Staff contract signature: container", ), perm( "a3000001-0001-4000-8000-00000000000a", "edr_freight_app:contracts:clearance_review", "Review pre-booking clearance docs", ), perm( "a3000001-0001-4000-8000-00000000000b", "edr_freight_app:contracts:finalize_clearance", "Finalize pre-booking clearance", ), perm( "a3000001-0001-4000-8000-00000000000c", "edr_freight_app:contracts:create_booking", "GL ET create booking under contract", ), perm( "a3000001-0001-4000-8000-00000000000d", "edr_freight_app:contracts:ops_clearance_review", "Operations review of self-clearance docs (Path A)", ), perm( "a3000001-0001-4000-8000-00000000000e", "edr_freight_app:contracts:clearance_et_actions", "GL Ethiopia phased clearance actions", ), perm( "a3000001-0001-4000-8000-00000000000f", "edr_freight_app:contracts:clearance_dj_actions", "GL Djibouti phased clearance actions", ), perm( "a3000001-0001-4000-8000-000000000010", "edr_freight_app:contracts:clearance_duty_advise", "Advise contract duty/tax", ), // Hazardous contracts get two extra approval steps ahead of the normal chain. // Each has its own permission so the two desks are genuinely separate people. perm( "a3000001-0001-4000-8000-000000000019", "edr_freight_app:contracts:hazardous_approval_one", "Hazardous approval — first review", ), perm( "a3000001-0001-4000-8000-00000000001a", "edr_freight_app:contracts:hazardous_approval_two", "Hazardous approval — second review", ), // Freeze/unfreeze a signed contract. One key covers both directions — whoever // may suspend must be able to lift it again. perm( "a3000001-0001-4000-8000-00000000001b", "edr_freight_app:contracts:suspend", "Suspend / resume a signed contract", ), ]; // Historical ids. EdrOrgSeeder no longer sends them — it upserts on `key` and // lets the column default mint the uuid — so these are kept only as a record of // which ids each environment already holds. A hand-picked id must still never // be recycled from a retired key: `edr_freight_app:rule_engine:truck_types:manage` // owned …001b, and reusing it for transit-agents crashed boot with a PK 23505 // on every environment that still had the retired row. const RULE_ENGINE_VIEW_IDS: Record = { "cargo-types": "b2000001-0001-4000-8000-000000000001", "container-types": "b2000001-0001-4000-8000-000000000003", "wagon-types": "b2000001-0001-4000-8000-000000000015", "truck-types": "b2000001-0001-4000-8000-00000000001a", "service-types": "b2000001-0001-4000-8000-000000000005", yards: "b2000001-0001-4000-8000-000000000007", "shipping-lines": "b2000001-0001-4000-8000-000000000009", "weight-limit-rules": "b2000001-0001-4000-8000-00000000000b", "priority-configs": "b2000001-0001-4000-8000-00000000000f", rates: "b2000001-0001-4000-8000-000000000011", "approval-rules": "b2000001-0001-4000-8000-000000000013", "yard-distances": "b2000001-0001-4000-8000-000000000018", "transit-agents": "b2000003-0001-4000-8000-000000000001", "operations-targets": "b2000003-0001-4000-8000-000000000002", }; // CRUD replaces the retired coarse `:manage`. New ids live in a fresh block // (b2000002-…) so a stale `:manage` grant can never silently confer a CRUD // action — the migration re-grants create/update/delete explicitly. const RULE_ENGINE_CRUD_ACTIONS = ["create", "update", "delete"] as const; type RuleEngineCrudAction = (typeof RULE_ENGINE_CRUD_ACTIONS)[number]; const ruleEngineCrudId = ( slug: RuleEngineResourceSlug, action: RuleEngineCrudAction, ): string => { const n = RULE_ENGINE_RESOURCE_SLUGS.indexOf(slug) * 3 + RULE_ENGINE_CRUD_ACTIONS.indexOf(action) + 1; // 1..36 return `b2000002-0001-4000-8000-${n.toString(16).padStart(12, "0")}`; }; /** * Slugs whose changes go through a separate approver. CRUD lets a staff member * propose a change; only `approve` lets someone put it into effect. Only listed * slugs get the permission. */ const RULE_ENGINE_APPROVE_PERMISSION_IDS: Partial< Record > = { rates: "b2000001-0001-4000-8000-000000000017", }; export type RuleEngineApprovableSlug = "rates"; export const RULE_ENGINE_PERMISSIONS: FreightPermissionSeed[] = RULE_ENGINE_RESOURCE_SLUGS.flatMap((slug) => { const resource = slugToResourceKey(slug); const approveId = RULE_ENGINE_APPROVE_PERMISSION_IDS[slug]; return [ perm( RULE_ENGINE_VIEW_IDS[slug], `edr_freight_app:rule_engine:${resource}:view`, `View ${slug}`, ), perm( ruleEngineCrudId(slug, "create"), `edr_freight_app:rule_engine:${resource}:create`, `Create ${slug}`, ), perm( ruleEngineCrudId(slug, "update"), `edr_freight_app:rule_engine:${resource}:update`, `Update ${slug}`, ), perm( ruleEngineCrudId(slug, "delete"), `edr_freight_app:rule_engine:${resource}:delete`, `Delete ${slug}`, ), ...(approveId ? [ perm( approveId, `edr_freight_app:rule_engine:${resource}:approve`, `Approve ${slug} changes`, ), ] : []), ]; }); /** * Container-allocation permission for the previously-unguarded * booking allocate-containers endpoint. */ export const GAP_CONTROLLER_PERMISSIONS: FreightPermissionSeed[] = [ perm( "c1000001-0001-4000-8000-000000000001", "edr_freight_app:allocation:manage", "Allocate containers to vehicles", ), ]; /** * Yard access scoping. `yard_positions` maps desks to yards and the resolver * (`YardScopeService`) narrows a caller to the yards their active position is * mapped to. This key is the deliberate way out of that narrowing, for the HQ * desks that are cross-yard by nature (OCC, CEO, rolling stock). Without it, * "unmapped" would have to mean "sees everything", which is a bypass by * accident rather than by grant. * * Editing the mapping itself needs no key of its own: it is yard configuration, * so it rides on `rule_engine:yards:view` / `:update` like every other field on * a yard. */ export const YARD_SCOPE_PERMISSIONS: FreightPermissionSeed[] = [ perm( "f4a00001-0001-4000-8000-000000000001", "edr_freight_app:yards:view_all", "Access every yard (bypass yard scoping)", ), ]; /** * Advanced backoffice resources — full CRUD + workflow-action keys. * See docs/rbac/freight-backoffice-permissions.md. Additive only: the existing * bookings/contracts/rule-engine/allocation keys above are unchanged. */ // C. Customers export const CUSTOMER_PERMISSIONS: FreightPermissionSeed[] = [ perm( "d1a00001-0001-4000-8000-000000000001", "edr_freight_app:customers:view", "View customers", ), perm( "d1a00001-0001-4000-8000-000000000002", "edr_freight_app:customers:create", "Create customer", ), perm( "d1a00001-0001-4000-8000-000000000003", "edr_freight_app:customers:update", "Update customer", ), perm( "d1a00001-0001-4000-8000-000000000004", "edr_freight_app:customers:deactivate", "Deactivate customer", ), perm( "d1a00001-0001-4000-8000-000000000005", "edr_freight_app:customers:verify", "Verify customer (KYC/Fayda)", ), perm( "d1a00001-0001-4000-8000-000000000006", "edr_freight_app:customers:reset-password", "Trigger customer password reset", ), ]; // C2. Shipping lines — carriers registered by staff (no self-signup). export const SHIPPING_LINE_PERMISSIONS: FreightPermissionSeed[] = [ perm( "d1a00002-0001-4000-8000-000000000001", "edr_freight_app:shipping_lines:view", "View shipping lines", ), perm( "d1a00002-0001-4000-8000-000000000002", "edr_freight_app:shipping_lines:create", "Register shipping line", ), perm( "d1a00002-0001-4000-8000-000000000003", "edr_freight_app:shipping_lines:update", "Update shipping line", ), perm( "d1a00002-0001-4000-8000-000000000004", "edr_freight_app:shipping_lines:reset-password", "Resend shipping line activation link", ), ]; // Internal chat (Matrix/Element) — sidebar visibility + manual reconcile trigger. export const CHAT_PERMISSIONS: FreightPermissionSeed[] = [ perm( "c9a00001-0001-4000-8000-000000000001", "edr_freight_app:chat:view", "Open internal chat", ), perm( "c9a00001-0001-4000-8000-000000000002", "edr_freight_app:chat:sync", "Re-run chat room/membership sync", ), ]; // D. Finance — payments + invoices export const FINANCE_PERMISSIONS: FreightPermissionSeed[] = [ perm( "d2a00001-0001-4000-8000-000000000001", "edr_freight_app:payments:view", "View payments", ), perm( "d2b00001-0001-4000-8000-000000000001", "edr_freight_app:invoices:view", "View invoices", ), perm( "d2b00001-0001-4000-8000-000000000004", "edr_freight_app:invoices:export", "Download invoice document", ), // Filing with the tax authority is its own grant: registration is irreversible at MoR, so it // must not ride along with the right to download an invoice PDF. perm( "d2b00001-0001-4000-8000-000000000005", "edr_freight_app:invoices:eims_register", "Register invoice with MoR EIMS", ), // Separate from registering: resolving an unacknowledged submission clears the // system-wide chain block and can record an IRN against an invoice, so it is a // supervisor/admin action rather than an operational one. perm( "d2b00001-0001-4000-8000-000000000006", "edr_freight_app:invoices:eims_resolve", "Resolve a blocked MoR EIMS submission", ), // Cancellation is a separate irreversible-at-MoR action from registration — its own grant, // same reasoning as eims_register. perm( "d2b00001-0001-4000-8000-000000000008", "edr_freight_app:invoices:eims_cancel", "Cancel a registered invoice with MoR EIMS", ), // Covers both sales and withholding receipts — same risk profile (filing a document with // MoR), no reason to split further. perm( "d2b00001-0001-4000-8000-000000000009", "edr_freight_app:invoices:eims_receipt_register", "Register a sales or withholding receipt with MoR EIMS", ), // Issuing a credit/debit memo is itself filing-equivalent — auto-submit picks it up like any // other issued invoice — so it carries the same restricted grant as the eims_* actions above, // not invoices:export. perm( "d2b00001-0001-4000-8000-00000000000a", "edr_freight_app:invoices:memo_issue", "Issue a credit or debit memo against a registered invoice", ), // USD bookings are paid by bank transfer; Finance uploads the slip and settles // the invoice. Moves money state, so it is its own grant, not part of view. perm( "d2b00001-0001-4000-8000-000000000007", "edr_freight_app:invoices:confirm_offline", "Confirm offline (bank transfer) invoice payment", ), // Shipping lines consume services on credit and are invoiced after the fact, // so what they owe is its own Finance surface, separate from invoices:view — // an unbilled credit is not an invoice yet. perm( "d2c00001-0001-4000-8000-000000000001", "edr_freight_app:shipping_line_credits:view", "View shipping-line credits and outstanding balance", ), perm( "d2c00001-0001-4000-8000-000000000002", "edr_freight_app:shipping_line_credits:invoice", "Generate an invoice from shipping-line credits", ), // Erases a debt outright, which is why it is not folded into :invoice. perm( "d2c00001-0001-4000-8000-000000000003", "edr_freight_app:shipping_line_credits:cancel", "Cancel (write off) an unbilled shipping-line credit", ), // Two-step manual actions on credit invoices: request grants per action, // decision grants that apply to any pending request. perm( "d2c00001-0001-4000-8000-000000000004", "edr_freight_app:shipping_line_credits:invoice_mark_paid", "Request marking a shipping-line credit invoice paid (offline payment)", ), perm( "d2c00001-0001-4000-8000-000000000005", "edr_freight_app:shipping_line_credits:invoice_approve", "Approve any pending shipping-line credit invoice request", ), perm( "d2c00001-0001-4000-8000-000000000006", "edr_freight_app:shipping_line_credits:invoice_cancel", "Request cancelling a shipping-line credit invoice", ), perm( "d2c00001-0001-4000-8000-000000000007", "edr_freight_app:shipping_line_credits:invoice_reject", "Reject any pending shipping-line credit invoice request", ), ]; // E. First / last mile operations export const MILE_PERMISSIONS: FreightPermissionSeed[] = [ perm( "d3a00001-0001-4000-8000-000000000001", "edr_freight_app:first_mile:view", "View first-mile", ), perm( "d3a00001-0001-4000-8000-000000000002", "edr_freight_app:first_mile:accept", "Accept first-mile request", ), perm( "d3a00001-0001-4000-8000-000000000003", "edr_freight_app:first_mile:create", "Create first-mile", ), perm( "d3a00001-0001-4000-8000-000000000004", "edr_freight_app:first_mile:update", "Update first-mile", ), perm( "d3a00001-0001-4000-8000-000000000005", "edr_freight_app:first_mile:delete", "Delete first-mile", ), perm( "d3a00001-0001-4000-8000-000000000006", "edr_freight_app:first_mile:assign_vehicles", "Assign first-mile vehicles", ), perm( "d3a00001-0001-4000-8000-000000000007", "edr_freight_app:first_mile:set_distances", "Set first-mile distances", ), perm( "d3a00001-0001-4000-8000-000000000008", "edr_freight_app:first_mile:generate_invoice", "Generate first-mile invoice", ), perm( "d3b00001-0001-4000-8000-000000000001", "edr_freight_app:last_mile:view", "View last-mile", ), perm( "d3b00001-0001-4000-8000-000000000002", "edr_freight_app:last_mile:accept", "Accept last-mile request", ), perm( "d3b00001-0001-4000-8000-000000000003", "edr_freight_app:last_mile:create", "Create last-mile", ), perm( "d3b00001-0001-4000-8000-000000000004", "edr_freight_app:last_mile:update", "Update last-mile", ), perm( "d3b00001-0001-4000-8000-000000000005", "edr_freight_app:last_mile:delete", "Delete last-mile", ), perm( "d3b00001-0001-4000-8000-000000000006", "edr_freight_app:last_mile:assign_vehicles", "Assign last-mile vehicles", ), perm( "d3b00001-0001-4000-8000-000000000007", "edr_freight_app:last_mile:set_distances", "Set last-mile distances", ), perm( "d3b00001-0001-4000-8000-000000000008", "edr_freight_app:last_mile:generate_invoice", "Generate last-mile invoice", ), perm( "d3b00001-0001-4000-8000-000000000009", "edr_freight_app:last_mile:request_view", "View last-mile confirmation requests", ), perm( "d3b00001-0001-4000-8000-00000000000a", "edr_freight_app:last_mile:request_review", "Review last-mile confirmation requests (T&M dept)", ), perm( "d3b00001-0001-4000-8000-00000000000b", "edr_freight_app:last_mile:request_approve", "Approve/reject last-mile confirmation requests", ), ]; // F. Fleet — rail assets (splits the flat fleet:view/manage) export const FLEET_RAIL_PERMISSIONS: FreightPermissionSeed[] = [ perm( "e1a00001-0001-4000-8000-000000000001", "edr_freight_app:locomotives:view", "View locomotives", ), perm( "e1a00001-0001-4000-8000-000000000002", "edr_freight_app:locomotives:create", "Create locomotive", ), perm( "e1a00001-0001-4000-8000-000000000003", "edr_freight_app:locomotives:update", "Update locomotive", ), perm( "e1a00001-0001-4000-8000-000000000004", "edr_freight_app:locomotives:delete", "Delete locomotive", ), perm( "e1a00001-0001-4000-8000-000000000005", "edr_freight_app:locomotives:hard_delete", "Permanently delete locomotive", ), perm( "e1b00001-0001-4000-8000-000000000001", "edr_freight_app:wagons:view", "View wagons", ), perm( "e1b00001-0001-4000-8000-000000000002", "edr_freight_app:wagons:create", "Create wagon", ), perm( "e1b00001-0001-4000-8000-000000000003", "edr_freight_app:wagons:update", "Update wagon", ), perm( "e1b00001-0001-4000-8000-000000000004", "edr_freight_app:wagons:delete", "Delete wagon", ), perm( "e1b00001-0001-4000-8000-000000000005", "edr_freight_app:wagons:transfer_request", "Request wagon transfer", ), perm( "e1b00001-0001-4000-8000-000000000006", "edr_freight_app:wagons:transfer_fulfill", "Fulfil wagon transfer (OCC)", ), perm( "e1b00001-0001-4000-8000-000000000007", "edr_freight_app:wagons:transfer_history_all", "View all staff's transfer history", ), // The transfer desk is its own screen, so it carries its own per-action keys — // seeing the queue, withdrawing a request and short-closing one are separate // grants from filing or fulfilling. perm( "e1b00001-0001-4000-8000-000000000008", "edr_freight_app:wagons:transfer_view", "View wagon transfer requests", ), perm( "e1b00001-0001-4000-8000-000000000009", "edr_freight_app:wagons:transfer_cancel", "Withdraw a wagon transfer request", ), perm( "e1b00001-0001-4000-8000-00000000000a", "edr_freight_app:wagons:transfer_close_short", "Close a transfer request short of the requested count", ), perm( "e1b00001-0001-4000-8000-00000000000b", "edr_freight_app:wagons:hard_delete", "Permanently delete wagon", ), // Maintenance ⇄ availability flip on the wagons desk — its own key so // operations/OCC can flip readiness without holding full wagon edit. perm( "e1b00001-0001-4000-8000-00000000000c", "edr_freight_app:wagons:status_toggle", "Flip wagon between maintenance and available", ), perm( "e1c00001-0001-4000-8000-000000000001", "edr_freight_app:trains:view", "View trains", ), perm( "e1c00001-0001-4000-8000-000000000002", "edr_freight_app:trains:create", "Create train", ), perm( "e1c00001-0001-4000-8000-000000000003", "edr_freight_app:trains:update", "Update train", ), perm( "e1c00001-0001-4000-8000-000000000004", "edr_freight_app:trains:delete", "Delete train", ), perm( "e1c00001-0001-4000-8000-000000000005", "edr_freight_app:trains:assign_wagons", "Assign wagons to train", ), // Granular splits of trains:update / trains:delete for the train-builder // detail page's Actions menu — each item gets its own grant instead of // sharing the coarse update/delete keys. perm( "e1c00001-0001-4000-8000-000000000006", "edr_freight_app:trains:change_locomotives", "Change train locomotives", ), perm( "e1c00001-0001-4000-8000-000000000007", "edr_freight_app:trains:change_yard", "Change train yard", ), perm( "e1c00001-0001-4000-8000-000000000008", "edr_freight_app:trains:toggle_active", "Activate or deactivate train", ), perm( "e1c00001-0001-4000-8000-000000000009", "edr_freight_app:trains:disband", "Disband train", ), perm( "e1c00001-0001-4000-8000-000000000010", "edr_freight_app:trains:change_wagon_yard", "Change yard of a coupled wagon", ), perm( "e1d00001-0001-4000-8000-000000000001", "edr_freight_app:routes:view", "View routes", ), perm( "e1d00001-0001-4000-8000-000000000002", "edr_freight_app:routes:create", "Create route", ), perm( "e1d00001-0001-4000-8000-000000000003", "edr_freight_app:routes:update", "Update route", ), perm( "e1d00001-0001-4000-8000-000000000004", "edr_freight_app:routes:delete", "Delete route", ), perm( "e1d00001-0001-4000-8000-000000000005", "edr_freight_app:routes:hard_delete", "Permanently delete route", ), perm( "e1e00001-0001-4000-8000-000000000001", "edr_freight_app:containers:view", "View containers", ), perm( "e1e00001-0001-4000-8000-000000000002", "edr_freight_app:containers:create", "Create container", ), perm( "e1e00001-0001-4000-8000-000000000003", "edr_freight_app:containers:update", "Update container", ), perm( "e1e00001-0001-4000-8000-000000000004", "edr_freight_app:containers:delete", "Delete container", ), perm( "e1f00001-0001-4000-8000-000000000001", "edr_freight_app:cargoes:view", "View cargoes", ), perm( "e1f00001-0001-4000-8000-000000000002", "edr_freight_app:cargoes:create", "Create cargo", ), perm( "e1f00001-0001-4000-8000-000000000003", "edr_freight_app:cargoes:update", "Update cargo", ), perm( "e1f00001-0001-4000-8000-000000000004", "edr_freight_app:cargoes:delete", "Delete cargo", ), // NB: id prefixes must stay hex — 'e1g…' once crashed the boot seeder // (postgres: invalid input syntax for type uuid). perm( "e1900001-0001-4000-8000-000000000001", "edr_freight_app:consignments:view", "View consignments", ), perm( "e1900001-0001-4000-8000-000000000002", "edr_freight_app:consignments:create", "Create consignment", ), ]; // G. Fleet — road & telemetry export const FLEET_ROAD_PERMISSIONS: FreightPermissionSeed[] = [ perm( "e2a00001-0001-4000-8000-000000000001", "edr_freight_app:vehicles:view", "View vehicles", ), perm( "e2a00001-0001-4000-8000-000000000002", "edr_freight_app:vehicles:create", "Create vehicle", ), perm( "e2a00001-0001-4000-8000-000000000003", "edr_freight_app:vehicles:update", "Update vehicle", ), perm( "e2a00001-0001-4000-8000-000000000004", "edr_freight_app:vehicles:delete", "Delete vehicle", ), perm( "e2b00001-0001-4000-8000-000000000001", "edr_freight_app:drivers:view", "View drivers", ), perm( "e2b00001-0001-4000-8000-000000000002", "edr_freight_app:drivers:create", "Create driver", ), perm( "e2b00001-0001-4000-8000-000000000003", "edr_freight_app:drivers:update", "Update driver", ), perm( "e2b00001-0001-4000-8000-000000000004", "edr_freight_app:drivers:delete", "Delete driver", ), perm( "e2c00001-0001-4000-8000-000000000001", "edr_freight_app:tracking:view", "Track vehicles", ), perm( "e2c00001-0001-4000-8000-000000000002", "edr_freight_app:tracking:manage", "Manage GPS trackers", ), perm( "e2d00001-0001-4000-8000-000000000001", "edr_freight_app:fuel:view", "View fuel purchases", ), perm( "e2d00001-0001-4000-8000-000000000002", "edr_freight_app:fuel:create", "Create fuel purchase", ), perm( "e2d00001-0001-4000-8000-000000000003", "edr_freight_app:fuel:update", "Update fuel purchase", ), perm( "e2d00001-0001-4000-8000-000000000004", "edr_freight_app:fuel:delete", "Delete fuel purchase", ), perm( "e2e00001-0001-4000-8000-000000000001", "edr_freight_app:maintenance:view", "View maintenance", ), perm( "e2e00001-0001-4000-8000-000000000002", "edr_freight_app:maintenance:create", "Create maintenance", ), perm( "e2e00001-0001-4000-8000-000000000003", "edr_freight_app:maintenance:update", "Update maintenance", ), perm( "e2e00001-0001-4000-8000-000000000004", "edr_freight_app:maintenance:delete", "Delete maintenance", ), perm( "e2f00001-0001-4000-8000-000000000001", "edr_freight_app:fleet_reports:view", "View fleet financial reports", ), perm( "e2f00001-0001-4000-8000-000000000002", "edr_freight_app:fleet_reports:export", "Export fleet financial reports", ), perm( "e2000001-0001-4000-8000-000000000001", "edr_freight_app:fleet_dashboard:view", "View fleet dashboard", ), ]; // H. Warehouse management export const WAREHOUSE_PERMISSIONS: FreightPermissionSeed[] = [ perm( "f1000001-0001-4000-8000-000000000001", "edr_freight_app:warehouse_dashboard:view", "View warehouse dashboard", ), perm( "f1a00001-0001-4000-8000-000000000001", "edr_freight_app:warehouses:view", "View warehouses", ), perm( "f1a00001-0001-4000-8000-000000000002", "edr_freight_app:warehouses:create", "Create warehouse", ), perm( "f1a00001-0001-4000-8000-000000000003", "edr_freight_app:warehouses:update", "Update warehouse", ), perm( "f1a00001-0001-4000-8000-000000000004", "edr_freight_app:warehouses:delete", "Delete warehouse", ), perm( "f1b00001-0001-4000-8000-000000000001", "edr_freight_app:warehouse_yards:view", "View warehouse yards", ), perm( "f1b00001-0001-4000-8000-000000000002", "edr_freight_app:warehouse_yards:create", "Create warehouse yard", ), perm( "f1b00001-0001-4000-8000-000000000003", "edr_freight_app:warehouse_yards:update", "Update warehouse yard", ), perm( "f1b00001-0001-4000-8000-000000000004", "edr_freight_app:warehouse_yards:delete", "Delete warehouse yard", ), perm( "f1c00001-0001-4000-8000-000000000001", "edr_freight_app:warehouse_zones:view", "View warehouse zones", ), perm( "f1c00001-0001-4000-8000-000000000002", "edr_freight_app:warehouse_zones:create", "Create warehouse zone", ), perm( "f1c00001-0001-4000-8000-000000000003", "edr_freight_app:warehouse_zones:update", "Update warehouse zone", ), perm( "f1d00001-0001-4000-8000-000000000001", "edr_freight_app:warehouse_allocation_rules:view", "View allocation rules", ), perm( "f1d00001-0001-4000-8000-000000000002", "edr_freight_app:warehouse_allocation_rules:create", "Create allocation rule", ), perm( "f1d00001-0001-4000-8000-000000000003", "edr_freight_app:warehouse_allocation_rules:update", "Update allocation rule", ), perm( "f1d00001-0001-4000-8000-000000000004", "edr_freight_app:warehouse_allocation_rules:delete", "Delete allocation rule", ), perm( "f1e00001-0001-4000-8000-000000000001", "edr_freight_app:warehouse_fee_rules:view", "View fee rules", ), perm( "f1e00001-0001-4000-8000-000000000002", "edr_freight_app:warehouse_fee_rules:create", "Create fee rule", ), perm( "f1e00001-0001-4000-8000-000000000003", "edr_freight_app:warehouse_fee_rules:update", "Update fee rule", ), perm( "f1e00001-0001-4000-8000-000000000004", "edr_freight_app:warehouse_fee_rules:delete", "Delete fee rule", ), perm( "f1f00001-0001-4000-8000-000000000001", "edr_freight_app:warehouse_inspection_reports:view", "View inspection reports", ), perm( "f1f00001-0001-4000-8000-000000000002", "edr_freight_app:warehouse_inspection_reports:create", "Create inspection report", ), perm( "f1f00001-0001-4000-8000-000000000003", "edr_freight_app:warehouse_inspection_reports:update", "Update inspection report", ), ]; // I. Port & terminal — inventory movement + interchange + fee invoices export const PORT_TERMINAL_PERMISSIONS: FreightPermissionSeed[] = [ perm( "f2a00001-0001-4000-8000-000000000001", "edr_freight_app:warehouse_inventory:view", "View terminal inventory", ), perm( "f2a00001-0001-4000-8000-000000000002", "edr_freight_app:warehouse_inventory:receive", "Receive inventory", ), perm( "f2a00001-0001-4000-8000-000000000003", "edr_freight_app:warehouse_inventory:move", "Move/store/reserve inventory", ), perm( "f2a00001-0001-4000-8000-000000000004", "edr_freight_app:warehouse_inventory:load", "Load inventory", ), perm( "f2a00001-0001-4000-8000-000000000005", "edr_freight_app:warehouse_inventory:unload", "Unload inventory", ), perm( "f2a00001-0001-4000-8000-000000000006", "edr_freight_app:warehouse_inventory:dispatch", "Dispatch inventory", ), perm( "f2a00001-0001-4000-8000-000000000007", "edr_freight_app:warehouse_inventory:gate_pass", "Gate-clearance inventory", ), perm( "f2a00001-0001-4000-8000-000000000008", "edr_freight_app:warehouse_inventory:release", "Release inventory", ), perm( "f2a00001-0001-4000-8000-000000000009", "edr_freight_app:warehouse_inventory:deliver", "Deliver inventory", ), perm( "f2a00001-0001-4000-8000-00000000000a", "edr_freight_app:warehouse_inventory:inspect", "Inspect inventory", ), perm( "f2b00001-0001-4000-8000-000000000001", "edr_freight_app:interchange_documents:view", "View interchange documents", ), perm( "f2b00001-0001-4000-8000-000000000002", "edr_freight_app:interchange_documents:generate", "Generate interchange document", ), perm( "f2b00001-0001-4000-8000-000000000003", "edr_freight_app:interchange_documents:acknowledge", "Acknowledge interchange document", ), perm( "f2b00001-0001-4000-8000-000000000004", "edr_freight_app:interchange_documents:dispute", "Dispute interchange document", ), perm( "f2b00001-0001-4000-8000-000000000005", "edr_freight_app:interchange_documents:cancel", "Cancel interchange document", ), perm( "f2c00001-0001-4000-8000-000000000001", "edr_freight_app:warehouse_fee_invoices:view", "View warehouse fee invoices", ), perm( "f2c00001-0001-4000-8000-000000000002", "edr_freight_app:warehouse_fee_invoices:generate", "Generate warehouse fee invoice", ), perm( "f2c00001-0001-4000-8000-000000000003", "edr_freight_app:warehouse_fee_invoices:cancel", "Cancel warehouse fee invoice", ), perm( "f2c00001-0001-4000-8000-000000000004", "edr_freight_app:warehouse_fee_invoices:pay", "Pay warehouse fee invoice", ), ]; // E. Additional charges — ad-hoc finance charges raised against a booking export const ADDITIONAL_CHARGE_PERMISSIONS: FreightPermissionSeed[] = [ perm( "f2e00001-0001-4000-8000-000000000001", "edr_freight_app:additional_charges:view", "View additional charges", ), perm( "f2e00001-0001-4000-8000-000000000002", "edr_freight_app:additional_charges:create", "Create additional charge", ), perm( "f2e00001-0001-4000-8000-000000000003", "edr_freight_app:additional_charges:send", "Send additional charge to customer", ), perm( "f2e00001-0001-4000-8000-000000000004", "edr_freight_app:additional_charges:cancel", "Cancel additional charge", ), ]; // E'. Train-scheduling finer actions (augment existing view/manage) export const SCHEDULING_EXTRA_PERMISSIONS: FreightPermissionSeed[] = [ perm( "a2a00001-0001-4000-8000-000000000001", "edr_freight_app:train_scheduling:create", "Create train schedule", ), perm( "a2a00001-0001-4000-8000-000000000002", "edr_freight_app:train_scheduling:update", "Update train schedule", ), perm( "a2a00001-0001-4000-8000-000000000003", "edr_freight_app:train_scheduling:cancel", "Cancel train schedule", ), perm( "a2a00001-0001-4000-8000-000000000004", "edr_freight_app:train_scheduling:reschedule", "Reschedule train", ), perm( "a2a00001-0001-4000-8000-000000000005", "edr_freight_app:train_scheduling:rules_manage", "Manage global scheduling rules", ), // Carved out of the coarse `update` — confirming a booking's cargo loaded/ // unloaded at a yard, across import, export, and intercity movements alike // (the same schedules/:id/bookings/:bookingId/{load,unload} + intercity // routes serve all three directions). perm( "a2a00001-0001-4000-8000-000000000006", "edr_freight_app:train_scheduling:load", "Confirm cargo loaded (import, export, intercity)", ), perm( "a2a00001-0001-4000-8000-000000000007", "edr_freight_app:train_scheduling:unload", "Confirm cargo unloaded (import, export, intercity)", ), ]; // L. Administration & settings (split from the coarse admin umbrella) export const CONFIG_SETTINGS_PERMISSIONS: FreightPermissionSeed[] = [ perm( "b4a00001-0001-4000-8000-000000000001", "edr_freight_app:settings:file_upload:view", "View file-upload settings", ), perm( "b4a00001-0001-4000-8000-000000000002", "edr_freight_app:settings:file_upload:manage", "Manage file-upload settings", ), perm( "b4b00001-0001-4000-8000-000000000001", "edr_freight_app:settings:dropdown:view", "View dropdown settings", ), perm( "b4b00001-0001-4000-8000-000000000002", "edr_freight_app:settings:dropdown:manage", "Manage dropdown settings", ), perm( "b4b00002-0001-4000-8000-000000000001", "edr_freight_app:settings:stamp:view", "View the company stamp", ), perm( "b4b00002-0001-4000-8000-000000000002", "edr_freight_app:settings:stamp:manage", "Manage the company stamp", ), perm( "b4b00004-0001-4000-8000-000000000001", "edr_freight_app:settings:logo:view", "View the company logo", ), perm( "b4b00004-0001-4000-8000-000000000002", "edr_freight_app:settings:logo:manage", "Manage the company logo", ), // The per-officer approval teeter (ማህተም) — an individual's own stamp + // signature, not the company seal. It used to ride on settings:stamp:*, which // now gates the ONE company stamp; this key was split out when the two were // untangled. `settings:invoice_stamp:*` retired at the same time: it gated the // company stamp before the fold and is deliberately left orphaned in any DB // that already seeded it (the seeder upserts by key and never deletes). perm( "b4b00003-0001-4000-8000-000000000001", "edr_freight_app:settings:teeter:view", "View own approval teeter and signature", ), perm( "b4b00003-0001-4000-8000-000000000002", "edr_freight_app:settings:teeter:manage", "Manage own approval teeter and signature", ), perm( "b4c00001-0001-4000-8000-000000000001", "edr_freight_app:audit:view", "View audit logs", ), ]; // M. Staff / IAM admin — NEW keys only. The employee_registration / role_assignment // / hierarchy_* / position_types:view keys are seeded separately in edr-freight.seed.ts. export const STAFF_IAM_PERMISSIONS: FreightPermissionSeed[] = [ perm( "c2a00001-0001-4000-8000-000000000001", "edr_freight_app:staff:roles:view", "View roles", ), perm( "c2a00001-0001-4000-8000-000000000002", "edr_freight_app:staff:roles:create", "Create role", ), perm( "c2a00001-0001-4000-8000-000000000003", "edr_freight_app:staff:roles:update", "Update role", ), perm( "c2a00001-0001-4000-8000-000000000004", "edr_freight_app:staff:roles:delete", "Delete role", ), perm( "c2b00001-0001-4000-8000-000000000001", "edr_freight_app:staff:permissions:view", "View permission assignments", ), perm( "c2b00001-0001-4000-8000-000000000002", "edr_freight_app:staff:permissions:assign", "Assign permissions", ), perm( "c2c00001-0001-4000-8000-000000000001", "edr_freight_app:position_types:create", "Create position type", ), perm( "c2c00001-0001-4000-8000-000000000002", "edr_freight_app:position_types:update", "Update position type", ), perm( "c2c00001-0001-4000-8000-000000000003", "edr_freight_app:position_types:delete", "Delete position type", ), ]; // O. Granular splits of previously-shared keys: money/irreversible actions that // used to ride on a broader permission (staff_accept, train_scheduling:update, // the admin umbrella) get their own grant so departments can hold them apart. export const GRANULAR_SPLIT_PERMISSIONS: FreightPermissionSeed[] = [ perm( "a5a00001-0001-4000-8000-000000000001", "edr_freight_app:bookings:government_expedite", "Expedite a government booking (bypass payment)", ), perm( "a5b00001-0001-4000-8000-000000000001", "edr_freight_app:contracts:edit_document", "Edit contract document articles", ), perm( "a5b00001-0001-4000-8000-000000000002", "edr_freight_app:contracts:final_invoice_raise", "Raise final invoice (GL DJ)", ), perm( "a5b00001-0001-4000-8000-000000000003", "edr_freight_app:contracts:final_invoice_confirm", "Confirm final-invoice payment slip", ), perm( "a5c00001-0001-4000-8000-000000000001", "edr_freight_app:train_scheduling:dispatch", "Finalize / dispatch a train schedule", ), perm( "a5c00001-0001-4000-8000-000000000002", "edr_freight_app:train_scheduling:mark_paid", "Mark a reserved booking paid (staff)", ), perm( "a5c00001-0001-4000-8000-000000000003", "edr_freight_app:train_scheduling:expire_booking", "Expire a reserved booking", ), perm( "b4d00001-0001-4000-8000-000000000001", "edr_freight_app:settings:exchange_rate:view", "View exchange-rate settings", ), perm( "b4d00001-0001-4000-8000-000000000002", "edr_freight_app:settings:exchange_rate:manage", "Set the USD-ETB fallback rate", ), perm( "b5000001-0001-4000-8000-000000000001", "edr_freight_app:settings:operations_standards:view", "View operating standards", ), perm( "b5000001-0001-4000-8000-000000000002", "edr_freight_app:settings:operations_standards:manage", "Edit operating standards", ), perm( "b4d00001-0001-4000-8000-000000000003", "edr_freight_app:settings:manual_payment:view", "View the manual (offline) payment channel settings", ), perm( "b4d00001-0001-4000-8000-000000000004", "edr_freight_app:settings:manual_payment:manage", "Enable or disable manual invoice settlement per currency", ), perm( "b4e00001-0001-4000-8000-000000000001", "edr_freight_app:settings:contract_templates:view", "View contract templates", ), perm( "b4e00001-0001-4000-8000-000000000002", "edr_freight_app:settings:contract_templates:manage", "Edit contract templates & articles", ), // Granular split of contract-template access. `view` opens the sidebar page; // `read` is API-read-only for other pages that display template data — and is // NOT written out here: deriveReadPermissions mints the `:read` twin of every // `:view` key, so a hand-written one duplicates the key (Postgres 21000 on the // seeder's ON CONFLICT (key) insert) and carries a v4 id where twins are v5. perm( "b4e00001-0001-4000-8000-000000000003", "edr_freight_app:settings:contract_templates:create", "Create bulk contract templates", ), perm( "b4e00001-0001-4000-8000-000000000004", "edr_freight_app:settings:contract_templates:update", "Update contract templates & articles", ), perm( "b4e00001-0001-4000-8000-000000000005", "edr_freight_app:settings:contract_templates:delete", "Delete bulk contract templates", ), perm( "b4f00001-0001-4000-8000-000000000001", "edr_freight_app:settings:support_content:view", "View portal help & legal content", ), perm( "b4f00001-0001-4000-8000-000000000002", "edr_freight_app:settings:support_content:manage", "Edit portal help, FAQ & legal content", ), ]; // N. Previously-ungated staff surfaces (support inbox, procurement, compliance, // facilities) plus keys for routes that only had auth-no-permission gating // (staff directory, trade access) and the dashboard/report reads the frontend // already gated on but were never seeded (overview:view / reports:view). export const AUDIENCE_GAP_PERMISSIONS: FreightPermissionSeed[] = [ perm( "a4a00001-0001-4000-8000-000000000001", "edr_freight_app:support:agent_view", "View support inbox (agent)", ), perm( "a4a00001-0001-4000-8000-000000000002", "edr_freight_app:support:agent_send", "Reply / start support threads (agent)", ), perm( "a4b00001-0001-4000-8000-000000000001", "edr_freight_app:procurement:view", "View procurement & asset lifecycle", ), perm( "a4b00001-0001-4000-8000-000000000002", "edr_freight_app:procurement:vendor_manage", "Manage vendors", ), perm( "a4b00001-0001-4000-8000-000000000003", "edr_freight_app:procurement:acquisition_manage", "Manage asset acquisitions", ), perm( "a4b00001-0001-4000-8000-000000000004", "edr_freight_app:procurement:disposal_manage", "Manage asset disposals", ), perm( "a4c00001-0001-4000-8000-000000000001", "edr_freight_app:compliance:view", "View vehicle compliance", ), perm( "a4c00001-0001-4000-8000-000000000002", "edr_freight_app:compliance:manage", "Manage vehicle compliance", ), perm( "a4d00001-0001-4000-8000-000000000001", "edr_freight_app:facilities:view", "View facilities", ), perm( "a4d00001-0001-4000-8000-000000000002", "edr_freight_app:facilities:manage", "Manage facilities", ), perm( "a4e00001-0001-4000-8000-000000000001", "edr_freight_app:staff:users:view", "List staff users (pickers)", ), perm( "a4e00001-0001-4000-8000-000000000002", "edr_freight_app:trade_access:view", "View trade-direction access", ), perm( "a4e00001-0001-4000-8000-000000000003", "edr_freight_app:trade_access:manage", "Manage trade-direction access", ), perm( "a4f00001-0001-4000-8000-000000000001", "edr_freight_app:overview:view", "View backoffice overview dashboard", ), perm( "a4f00001-0001-4000-8000-000000000002", "edr_freight_app:reports:view", "Run backoffice reports", ), ]; // O. Notification recipient selectors. NOT route guards — these are never // passed to FreightPermissionGuard/assertFreightPermission and never appear in // the frontend's RequirePermission. They exist so ops can tune who gets pinged // WITHOUT changing who can open the page. Before them every staff notification // used the `allBackoffice` selector, i.e. every current employee in every org. export const NOTIFICATION_PERMISSIONS: FreightPermissionSeed[] = [ perm( "f3a00001-0001-4000-8000-000000000001", "edr_freight_app:bookings:get_notification", "Receive booking desk notifications", ), perm( "f3a00001-0001-4000-8000-000000000002", "edr_freight_app:bookings:clearance_get_notification", "Receive booking clearance notifications", ), perm( "f3a00001-0001-4000-8000-000000000003", "edr_freight_app:contracts:get_notification", "Receive contract desk notifications", ), perm( "f3a00001-0001-4000-8000-000000000004", "edr_freight_app:contracts:clearance_get_notification", "Receive contract clearance notifications", ), perm( "f3a00001-0001-4000-8000-000000000005", "edr_freight_app:customers:get_notification", "Receive customer desk notifications", ), perm( "f3a00001-0001-4000-8000-000000000006", "edr_freight_app:maintenance:get_notification", "Receive maintenance due notifications", ), perm( "f3a00001-0001-4000-8000-000000000007", "edr_freight_app:rule_engine:get_notification", "Receive rule-change approval notifications", ), perm( "f3a00001-0001-4000-8000-000000000008", "edr_freight_app:warehouse_fee_invoices:get_notification", "Receive warehouse fee accrual notifications", ), perm( "f3a00001-0001-4000-8000-000000000009", "edr_freight_app:additional_charges:get_notification", "Receive additional charge notifications", ), ]; export const ADVANCED_BACKOFFICE_PERMISSIONS: FreightPermissionSeed[] = [ ...REPORT_PERMISSIONS, ...OVERVIEW_LAYOUT_PERMISSIONS, ...CUSTOMER_PERMISSIONS, ...SHIPPING_LINE_PERMISSIONS, ...CHAT_PERMISSIONS, ...FINANCE_PERMISSIONS, ...MILE_PERMISSIONS, ...FLEET_RAIL_PERMISSIONS, ...FLEET_ROAD_PERMISSIONS, ...WAREHOUSE_PERMISSIONS, ...PORT_TERMINAL_PERMISSIONS, ...ADDITIONAL_CHARGE_PERMISSIONS, ...SCHEDULING_EXTRA_PERMISSIONS, ...CONFIG_SETTINGS_PERMISSIONS, ...STAFF_IAM_PERMISSIONS, ...AUDIENCE_GAP_PERMISSIONS, ...GRANULAR_SPLIT_PERMISSIONS, ...NOTIFICATION_PERMISSIONS, ]; export const BOOKING_RULE_ENGINE_PERMISSIONS = [ ...BOOKING_PERMISSIONS, ...CONTRACT_PERMISSIONS, ...RULE_ENGINE_PERMISSIONS, ...GAP_CONTROLLER_PERMISSIONS, ...YARD_SCOPE_PERMISSIONS, ...ADVANCED_BACKOFFICE_PERMISSIONS, ]; export const BOOKING_RULE_ENGINE_PERMISSION_KEYS = BOOKING_RULE_ENGINE_PERMISSIONS.map((p) => p.key); /** * Id for a derived `:read` key: the source `:view` id with its version nibble * moved 4 → 5. * * Unique because the source ids are, and disjoint from every hand-written id * because those are all v4-shaped. Nothing index-derived, so a new `:view` * landing mid-list cannot shift ids already seeded — the trap the * RULE_ENGINE_RESOURCE_SLUGS comment warns about. * * (`EdrOrgSeeder.ensurePermissions` deliberately never sends an id — the * column default wins and `key` is the identity every consumer resolves by. * These exist to satisfy the seed type and keep the array self-consistent.) */ const readPermissionId = (viewId: string): string => `${viewId.slice(0, 14)}5${viewId.slice(15)}`; /** * API-read twin of every `:view` key. * * `:view` does three jobs at once — backoffice sidebar entry (App.tsx), route * admission (RequirePermission), and API read. That bundling means a user who * only needs another module's list endpoint for a form dropdown has to be * granted the whole module, page and all. `:read` unbundles it: the * guard accepts it wherever the matching `:view` is required on a GET, and the * frontend never looks at it, so the module stays out of the menu. * * Derived rather than hand-written so a new `:view` key gets its twin for * free. Grants stay hand-curated in `iam.position_type_permissions` — nothing * here hands a `:read` to anyone. */ export const deriveReadPermissions = ( seeds: readonly FreightPermissionSeed[], ): FreightPermissionSeed[] => seeds .filter((p) => p.key.endsWith(VIEW_KEY_SUFFIX)) .map((p) => perm( readPermissionId(p.id), readTwinOf(p.key) as string, `Read ${p.name.en.replace(/^View /, "")} (API only)`, ), ); /** * Read twins of the freight-domain catalog, for `PERMISSIONS_CATALOG`. The * IAM/hierarchy keys seeded alongside it live in `edr-freight.seed.ts` and get * theirs there — each twin is derived from its own source row, so the two call * sites agree on any key they share without coordination. */ export const FREIGHT_READ_PERMISSIONS = deriveReadPermissions( BOOKING_RULE_ENGINE_PERMISSIONS, ); export const FREIGHT_PERMS = { bookings: { view: "edr_freight_app:bookings:view", create: "edr_freight_app:bookings:create", clearanceView: "edr_freight_app:bookings:clearance_view", staffAccept: "edr_freight_app:bookings:staff_accept", requestChanges: "edr_freight_app:bookings:request_changes", reject: "edr_freight_app:bookings:reject", approveLineStaff: "edr_freight_app:bookings:approve_line_staff", approveDirector: "edr_freight_app:bookings:approve_director", approveCeo: "edr_freight_app:bookings:approve_ceo", rejectApproval: "edr_freight_app:bookings:reject_approval", generateContract: "edr_freight_app:bookings:generate_contract", signStaff: "edr_freight_app:bookings:sign_staff", operations: "edr_freight_app:bookings:operations", cancel: "edr_freight_app:bookings:cancel", reviewDocuments: "edr_freight_app:bookings:review_documents", uploadClearanceOutput: "edr_freight_app:bookings:upload_clearance_output", finalizeClearance: "edr_freight_app:bookings:finalize_clearance", docReviewAlert: "edr_freight_app:bookings:doc_review_alert", governmentExpedite: "edr_freight_app:bookings:government_expedite", wagonCancellationView: "edr_freight_app:bookings:wagon_cancellation_view", wagonCancellationVoid: "edr_freight_app:bookings:wagon_cancellation_void", wagonCancellationRebook: "edr_freight_app:bookings:wagon_cancellation_rebook", approveConsolidation: "edr_freight_app:bookings:approve_consolidation", // Notification selectors, not route guards — see NOTIFICATION_PERMISSIONS. getNotification: "edr_freight_app:bookings:get_notification", clearanceGetNotification: "edr_freight_app:bookings:clearance_get_notification", }, contracts: { view: "edr_freight_app:contracts:view", staffAccept: { bulk: "edr_freight_app:contracts:staff_accept:bulk", container: "edr_freight_app:contracts:staff_accept:container", }, requestChanges: { bulk: "edr_freight_app:contracts:request_changes:bulk", container: "edr_freight_app:contracts:request_changes:container", }, reject: { bulk: "edr_freight_app:contracts:reject:bulk", container: "edr_freight_app:contracts:reject:container", }, approveLineStaff: "edr_freight_app:contracts:approve_line_staff", approveDirector: "edr_freight_app:contracts:approve_director", approveCeo: "edr_freight_app:contracts:approve_ceo", hazardousApprovalOne: "edr_freight_app:contracts:hazardous_approval_one", hazardousApprovalTwo: "edr_freight_app:contracts:hazardous_approval_two", generateContract: "edr_freight_app:contracts:generate_contract", signStaff: { bulk: "edr_freight_app:contracts:sign_staff:bulk", container: "edr_freight_app:contracts:sign_staff:container", }, clearanceReview: "edr_freight_app:contracts:clearance_review", finalizeClearance: "edr_freight_app:contracts:finalize_clearance", createBooking: "edr_freight_app:contracts:create_booking", opsClearanceReview: "edr_freight_app:contracts:ops_clearance_review", clearanceEtActions: "edr_freight_app:contracts:clearance_et_actions", clearanceDjActions: "edr_freight_app:contracts:clearance_dj_actions", clearanceDutyAdvise: "edr_freight_app:contracts:clearance_duty_advise", suspend: "edr_freight_app:contracts:suspend", editDocument: "edr_freight_app:contracts:edit_document", finalInvoiceRaise: "edr_freight_app:contracts:final_invoice_raise", finalInvoiceConfirm: "edr_freight_app:contracts:final_invoice_confirm", // Notification selectors, not route guards — see NOTIFICATION_PERMISSIONS. getNotification: "edr_freight_app:contracts:get_notification", clearanceGetNotification: "edr_freight_app:contracts:clearance_get_notification", }, trainScheduling: { view: "edr_freight_app:train_scheduling:view", create: "edr_freight_app:train_scheduling:create", update: "edr_freight_app:train_scheduling:update", cancel: "edr_freight_app:train_scheduling:cancel", reschedule: "edr_freight_app:train_scheduling:reschedule", rulesManage: "edr_freight_app:train_scheduling:rules_manage", /** * Confirm a booking's cargo loaded/unloaded at a yard — carved out of the * coarse `update` so load/unload can be granted independently of general * schedule editing. Covers import, export, and intercity alike: the * generic per-booking route and the intercity-specific one both gate on * these same two keys. */ load: "edr_freight_app:train_scheduling:load", unload: "edr_freight_app:train_scheduling:unload", dispatch: "edr_freight_app:train_scheduling:dispatch", markPaid: "edr_freight_app:train_scheduling:mark_paid", expireBooking: "edr_freight_app:train_scheduling:expire_booking", /** * Edit a schedule's operational run numbers (train + voyage) before * dispatch. Separate from `update`: these numbers are what yards and * customs quote, so changing them is narrower than general scheduling edits. */ editTrainNumber: "edr_freight_app:train_scheduling:edit_train_number", }, fleet: { view: "edr_freight_app:fleet:view", manage: "edr_freight_app:fleet:manage", }, /** * Audit trail. View-only: the module has no write routes, so this is the * only key it needs — see AUDIT_LOG_PERMISSIONS in edr-freight.seed.ts. */ auditLog: { view: "edr_freight_app:audit_log:view", }, admin: "edr_freight_app:admin", ruleEngine: { view: (slug: RuleEngineResourceSlug) => `edr_freight_app:rule_engine:${slugToResourceKey(slug)}:view`, create: (slug: RuleEngineResourceSlug) => `edr_freight_app:rule_engine:${slugToResourceKey(slug)}:create`, update: (slug: RuleEngineResourceSlug) => `edr_freight_app:rule_engine:${slugToResourceKey(slug)}:update`, delete: (slug: RuleEngineResourceSlug) => `edr_freight_app:rule_engine:${slugToResourceKey(slug)}:delete`, approve: (slug: RuleEngineApprovableSlug) => `edr_freight_app:rule_engine:${slugToResourceKey(slug)}:approve`, // Notification selector, not a route guard — see NOTIFICATION_PERMISSIONS. // One key for the whole rules desk: every preset grants the rule-engine // view keys as a block, and both producers link to /dashboard/rules/*. getNotification: "edr_freight_app:rule_engine:get_notification", }, allocation: { manage: "edr_freight_app:allocation:manage", }, yards: { /** Bypasses yard scoping entirely — see YARD_SCOPE_PERMISSIONS. */ viewAll: "edr_freight_app:yards:view_all", }, customers: { view: "edr_freight_app:customers:view", create: "edr_freight_app:customers:create", update: "edr_freight_app:customers:update", deactivate: "edr_freight_app:customers:deactivate", verify: "edr_freight_app:customers:verify", resetPassword: "edr_freight_app:customers:reset-password", // Notification selector, not a route guard — see NOTIFICATION_PERMISSIONS. getNotification: "edr_freight_app:customers:get_notification", }, shippingLines: { view: "edr_freight_app:shipping_lines:view", create: "edr_freight_app:shipping_lines:create", update: "edr_freight_app:shipping_lines:update", resetPassword: "edr_freight_app:shipping_lines:reset-password", }, shippingLineCredits: { view: "edr_freight_app:shipping_line_credits:view", /** Turn a batch of unbilled credits into an invoice. */ invoice: "edr_freight_app:shipping_line_credits:invoice", /** Write off an unbilled credit — separate grant: it erases a debt. */ cancel: "edr_freight_app:shipping_line_credits:cancel", // Two-step manual actions on credit invoices, gated purely by permission: // finance-level REQUEST grants (per action) and decision grants that apply // to ANY pending request — including the holder's own. /** Request recording an offline payment against a credit invoice. */ invoiceMarkPaid: "edr_freight_app:shipping_line_credits:invoice_mark_paid", /** Request voiding a credit invoice (credits return to unbilled). */ invoiceCancel: "edr_freight_app:shipping_line_credits:invoice_cancel", /** Approve any pending invoice request (mark-paid or cancel). */ invoiceApprove: "edr_freight_app:shipping_line_credits:invoice_approve", /** Reject any pending invoice request. */ invoiceReject: "edr_freight_app:shipping_line_credits:invoice_reject", }, chat: { view: "edr_freight_app:chat:view", sync: "edr_freight_app:chat:sync", }, payments: { view: "edr_freight_app:payments:view", }, invoices: { view: "edr_freight_app:invoices:view", export: "edr_freight_app:invoices:export", eimsRegister: "edr_freight_app:invoices:eims_register", eimsResolve: "edr_freight_app:invoices:eims_resolve", eimsCancel: "edr_freight_app:invoices:eims_cancel", eimsReceiptRegister: "edr_freight_app:invoices:eims_receipt_register", memoIssue: "edr_freight_app:invoices:memo_issue", confirmOffline: "edr_freight_app:invoices:confirm_offline", }, firstMile: { view: "edr_freight_app:first_mile:view", accept: "edr_freight_app:first_mile:accept", create: "edr_freight_app:first_mile:create", update: "edr_freight_app:first_mile:update", delete: "edr_freight_app:first_mile:delete", assignVehicles: "edr_freight_app:first_mile:assign_vehicles", setDistances: "edr_freight_app:first_mile:set_distances", generateInvoice: "edr_freight_app:first_mile:generate_invoice", }, lastMile: { view: "edr_freight_app:last_mile:view", accept: "edr_freight_app:last_mile:accept", create: "edr_freight_app:last_mile:create", update: "edr_freight_app:last_mile:update", delete: "edr_freight_app:last_mile:delete", assignVehicles: "edr_freight_app:last_mile:assign_vehicles", setDistances: "edr_freight_app:last_mile:set_distances", generateInvoice: "edr_freight_app:last_mile:generate_invoice", // Pre-approval confirmation stage (Truck & Machinery department): view/review // a submitted request, approve/reject it. requestView: "edr_freight_app:last_mile:request_view", requestReview: "edr_freight_app:last_mile:request_review", requestApprove: "edr_freight_app:last_mile:request_approve", }, locomotives: { view: "edr_freight_app:locomotives:view", create: "edr_freight_app:locomotives:create", update: "edr_freight_app:locomotives:update", delete: "edr_freight_app:locomotives:delete", /** * Permanently purge the row — irreversible, and separate from `delete` * (which only decommissions) so it can be granted to far fewer people. */ hardDelete: "edr_freight_app:locomotives:hard_delete", }, wagons: { view: "edr_freight_app:wagons:view", create: "edr_freight_app:wagons:create", update: "edr_freight_app:wagons:update", delete: "edr_freight_app:wagons:delete", /** Permanently purge the row — irreversible; see locomotives.hardDelete. */ hardDelete: "edr_freight_app:wagons:hard_delete", // Requester creates a transfer request; OCC fulfils it (picks the wagons and // executes the move). Distinct keys so OCC can hold fulfil without request. transferRequest: "edr_freight_app:wagons:transfer_request", transferFulfill: "edr_freight_app:wagons:transfer_fulfill", /** Open the transfer-requests desk (list + detail). */ transferView: "edr_freight_app:wagons:transfer_view", /** Withdraw a request that has not moved any wagon yet. */ transferCancel: "edr_freight_app:wagons:transfer_cancel", /** Maintenance ⇄ availability flip on the wagons desk (audited). */ statusToggle: "edr_freight_app:wagons:status_toggle", /** End a request short — anyone who can fulfil may also do this. */ transferCloseShort: "edr_freight_app:wagons:transfer_close_short", // Admin: read every staffer's transfer history. Without it, a user only sees // their own (the /history endpoint uses the caller id, backend-enforced). transferHistoryAll: "edr_freight_app:wagons:transfer_history_all", }, trains: { view: "edr_freight_app:trains:view", create: "edr_freight_app:trains:create", update: "edr_freight_app:trains:update", delete: "edr_freight_app:trains:delete", assignWagons: "edr_freight_app:trains:assign_wagons", changeLocomotives: "edr_freight_app:trains:change_locomotives", changeYard: "edr_freight_app:trains:change_yard", changeWagonYard: "edr_freight_app:trains:change_wagon_yard", toggleActive: "edr_freight_app:trains:toggle_active", disband: "edr_freight_app:trains:disband", }, routes: { view: "edr_freight_app:routes:view", create: "edr_freight_app:routes:create", update: "edr_freight_app:routes:update", delete: "edr_freight_app:routes:delete", /** Permanently purge the row — irreversible; see locomotives.hardDelete. */ hardDelete: "edr_freight_app:routes:hard_delete", }, containers: { view: "edr_freight_app:containers:view", create: "edr_freight_app:containers:create", update: "edr_freight_app:containers:update", delete: "edr_freight_app:containers:delete", }, cargoes: { view: "edr_freight_app:cargoes:view", create: "edr_freight_app:cargoes:create", update: "edr_freight_app:cargoes:update", delete: "edr_freight_app:cargoes:delete", }, consignments: { view: "edr_freight_app:consignments:view", create: "edr_freight_app:consignments:create", }, vehicles: { view: "edr_freight_app:vehicles:view", create: "edr_freight_app:vehicles:create", update: "edr_freight_app:vehicles:update", delete: "edr_freight_app:vehicles:delete", }, drivers: { view: "edr_freight_app:drivers:view", create: "edr_freight_app:drivers:create", update: "edr_freight_app:drivers:update", delete: "edr_freight_app:drivers:delete", }, tracking: { view: "edr_freight_app:tracking:view", manage: "edr_freight_app:tracking:manage", }, fuel: { view: "edr_freight_app:fuel:view", create: "edr_freight_app:fuel:create", update: "edr_freight_app:fuel:update", delete: "edr_freight_app:fuel:delete", }, maintenance: { view: "edr_freight_app:maintenance:view", create: "edr_freight_app:maintenance:create", update: "edr_freight_app:maintenance:update", delete: "edr_freight_app:maintenance:delete", // Notification selector, not a route guard — see NOTIFICATION_PERMISSIONS. getNotification: "edr_freight_app:maintenance:get_notification", }, fleetReports: { view: "edr_freight_app:fleet_reports:view", export: "edr_freight_app:fleet_reports:export", }, fleetDashboard: { view: "edr_freight_app:fleet_dashboard:view", }, warehouseDashboard: { view: "edr_freight_app:warehouse_dashboard:view", }, warehouses: { view: "edr_freight_app:warehouses:view", create: "edr_freight_app:warehouses:create", update: "edr_freight_app:warehouses:update", delete: "edr_freight_app:warehouses:delete", }, warehouseYards: { view: "edr_freight_app:warehouse_yards:view", create: "edr_freight_app:warehouse_yards:create", update: "edr_freight_app:warehouse_yards:update", delete: "edr_freight_app:warehouse_yards:delete", }, warehouseZones: { view: "edr_freight_app:warehouse_zones:view", create: "edr_freight_app:warehouse_zones:create", update: "edr_freight_app:warehouse_zones:update", }, warehouseAllocationRules: { view: "edr_freight_app:warehouse_allocation_rules:view", create: "edr_freight_app:warehouse_allocation_rules:create", update: "edr_freight_app:warehouse_allocation_rules:update", delete: "edr_freight_app:warehouse_allocation_rules:delete", }, warehouseFeeRules: { view: "edr_freight_app:warehouse_fee_rules:view", create: "edr_freight_app:warehouse_fee_rules:create", update: "edr_freight_app:warehouse_fee_rules:update", delete: "edr_freight_app:warehouse_fee_rules:delete", }, warehouseInspectionReports: { view: "edr_freight_app:warehouse_inspection_reports:view", create: "edr_freight_app:warehouse_inspection_reports:create", update: "edr_freight_app:warehouse_inspection_reports:update", }, warehouseInventory: { view: "edr_freight_app:warehouse_inventory:view", receive: "edr_freight_app:warehouse_inventory:receive", move: "edr_freight_app:warehouse_inventory:move", load: "edr_freight_app:warehouse_inventory:load", unload: "edr_freight_app:warehouse_inventory:unload", dispatch: "edr_freight_app:warehouse_inventory:dispatch", gatePass: "edr_freight_app:warehouse_inventory:gate_pass", release: "edr_freight_app:warehouse_inventory:release", deliver: "edr_freight_app:warehouse_inventory:deliver", inspect: "edr_freight_app:warehouse_inventory:inspect", }, interchangeDocuments: { view: "edr_freight_app:interchange_documents:view", generate: "edr_freight_app:interchange_documents:generate", acknowledge: "edr_freight_app:interchange_documents:acknowledge", dispute: "edr_freight_app:interchange_documents:dispute", cancel: "edr_freight_app:interchange_documents:cancel", }, warehouseFeeInvoices: { view: "edr_freight_app:warehouse_fee_invoices:view", generate: "edr_freight_app:warehouse_fee_invoices:generate", cancel: "edr_freight_app:warehouse_fee_invoices:cancel", pay: "edr_freight_app:warehouse_fee_invoices:pay", // Notification selector, not a route guard — see NOTIFICATION_PERMISSIONS. getNotification: "edr_freight_app:warehouse_fee_invoices:get_notification", }, additionalCharges: { view: "edr_freight_app:additional_charges:view", create: "edr_freight_app:additional_charges:create", send: "edr_freight_app:additional_charges:send", cancel: "edr_freight_app:additional_charges:cancel", // Notification selector, not a route guard — see NOTIFICATION_PERMISSIONS. getNotification: "edr_freight_app:additional_charges:get_notification", }, settings: { fileUpload: { view: "edr_freight_app:settings:file_upload:view", manage: "edr_freight_app:settings:file_upload:manage", }, dropdown: { view: "edr_freight_app:settings:dropdown:view", manage: "edr_freight_app:settings:dropdown:manage", }, // The ONE company stamp/seal, applied to every generated document // (invoices, receipts, warehouse papers, the EDR side of contracts). stamp: { view: "edr_freight_app:settings:stamp:view", manage: "edr_freight_app:settings:stamp:manage", }, // The ONE company logo, applied to every generated document (invoices, // receipts, contracts, warehouse papers, train-scheduling manifests). logo: { view: "edr_freight_app:settings:logo:view", manage: "edr_freight_app:settings:logo:manage", }, // The per-officer approval teeter (ማህተም) + signature — genuinely per-person, // and NOT the company seal above. Retired: `invoiceStamp`, which used to // gate the company stamp before the two were untangled. teeter: { view: "edr_freight_app:settings:teeter:view", manage: "edr_freight_app:settings:teeter:manage", }, exchangeRate: { view: "edr_freight_app:settings:exchange_rate:view", manage: "edr_freight_app:settings:exchange_rate:manage", }, // Standard station stay, cycle and leg times, and the charged-tonnage // factors the operations reports measure actual performance against. operationsStandards: { view: "edr_freight_app:settings:operations_standards:view", manage: "edr_freight_app:settings:operations_standards:manage", }, // Whether Finance may settle invoices by hand, per currency. Split // view/manage on purpose: Finance reads it (the worklist offers only the // enabled currencies) but must not switch its own channel on — same // maker-checker split as the other sensitive finance settings. manualPayment: { view: "edr_freight_app:settings:manual_payment:view", manage: "edr_freight_app:settings:manual_payment:manage", }, contractTemplates: { view: "edr_freight_app:settings:contract_templates:view", manage: "edr_freight_app:settings:contract_templates:manage", create: "edr_freight_app:settings:contract_templates:create", update: "edr_freight_app:settings:contract_templates:update", delete: "edr_freight_app:settings:contract_templates:delete", read: "edr_freight_app:settings:contract_templates:read", }, // Portal-facing help/FAQ/legal copy, edited from Portal content. supportContent: { view: "edr_freight_app:settings:support_content:view", manage: "edr_freight_app:settings:support_content:manage", }, }, support: { agentView: "edr_freight_app:support:agent_view", agentSend: "edr_freight_app:support:agent_send", }, procurement: { view: "edr_freight_app:procurement:view", vendorManage: "edr_freight_app:procurement:vendor_manage", acquisitionManage: "edr_freight_app:procurement:acquisition_manage", disposalManage: "edr_freight_app:procurement:disposal_manage", }, compliance: { view: "edr_freight_app:compliance:view", manage: "edr_freight_app:compliance:manage", }, facilities: { view: "edr_freight_app:facilities:view", manage: "edr_freight_app:facilities:manage", }, tradeAccess: { view: "edr_freight_app:trade_access:view", manage: "edr_freight_app:trade_access:manage", }, overview: { view: "edr_freight_app:overview:view", layout: (key: OverviewLayoutKey): string => overviewLayoutPermissionKey(key), }, reports: { view: "edr_freight_app:reports:view", report: (key: ReportKey): string => reportPermissionKey(key), }, staff: { users: { view: "edr_freight_app:staff:users:view", }, roles: { view: "edr_freight_app:staff:roles:view", create: "edr_freight_app:staff:roles:create", update: "edr_freight_app:staff:roles:update", delete: "edr_freight_app:staff:roles:delete", }, permissions: { view: "edr_freight_app:staff:permissions:view", assign: "edr_freight_app:staff:permissions:assign", }, // Seeded in edr-freight.seed.ts (EDR_FREIGHT_PERMISSIONS) — surfaced here for gating. employeeRegistration: { view: "edr_freight_app:employee_registration:view", create: "edr_freight_app:employee_registration:create", update: "edr_freight_app:employee_registration:update", activate: "edr_freight_app:employee_registration:activate", deactivate: "edr_freight_app:employee_registration:deactivate", }, roleAssignment: { view: "edr_freight_app:role_assignment:view", assign: "edr_freight_app:role_assignment:assign", replace: "edr_freight_app:role_assignment:replace", }, hierarchyUnits: { view: "edr_freight_app:hierarchy_units:view", create: "edr_freight_app:hierarchy_units:create", update: "edr_freight_app:hierarchy_units:update", delete: "edr_freight_app:hierarchy_units:delete", }, hierarchyPositions: { view: "edr_freight_app:hierarchy_positions:view", create: "edr_freight_app:hierarchy_positions:create", update: "edr_freight_app:hierarchy_positions:update", delete: "edr_freight_app:hierarchy_positions:delete", changeParent: "edr_freight_app:hierarchy_positions:change_parent", }, hierarchyEmployeeAssignment: { view: "edr_freight_app:hierarchy_employee_assignment:view", invite: "edr_freight_app:hierarchy_employee_assignment:invite", assign: "edr_freight_app:hierarchy_employee_assignment:assign", }, positionTypes: { view: "edr_freight_app:position_types:view", create: "edr_freight_app:position_types:create", update: "edr_freight_app:position_types:update", delete: "edr_freight_app:position_types:delete", }, }, } as const; /** * Backfill sources for the `:get_notification` keys: a new key is * granted to whoever already holds ANY of its anchors. The anchor is the key * that gates the page the notification deep-links to — if you cannot open the * page, you were never the intended recipient. The rule-engine desk has no page * of its own, so it anchors on the keys that let you ACT on a filed change. * * Consumed only by FreightNotificationPermissionsSeeder. Keeping it here means * the registry spec can assert every anchor still exists in the catalog — a * typo'd anchor backfills nobody, silently. */ export const NOTIFICATION_PERMISSION_ANCHORS: Record = { [FREIGHT_PERMS.bookings.getNotification]: [FREIGHT_PERMS.bookings.view], [FREIGHT_PERMS.bookings.clearanceGetNotification]: [ FREIGHT_PERMS.bookings.clearanceView, ], [FREIGHT_PERMS.contracts.getNotification]: [FREIGHT_PERMS.contracts.view], [FREIGHT_PERMS.contracts.clearanceGetNotification]: [ FREIGHT_PERMS.contracts.clearanceReview, FREIGHT_PERMS.contracts.clearanceEtActions, FREIGHT_PERMS.contracts.clearanceDjActions, FREIGHT_PERMS.contracts.opsClearanceReview, ], [FREIGHT_PERMS.customers.getNotification]: [FREIGHT_PERMS.customers.view], [FREIGHT_PERMS.maintenance.getNotification]: [FREIGHT_PERMS.maintenance.view], [FREIGHT_PERMS.ruleEngine.getNotification]: [ FREIGHT_PERMS.ruleEngine.approve("rates"), FREIGHT_PERMS.ruleEngine.update("priority-configs"), ], [FREIGHT_PERMS.warehouseFeeInvoices.getNotification]: [ FREIGHT_PERMS.warehouseFeeInvoices.view, ], [FREIGHT_PERMS.additionalCharges.getNotification]: [ FREIGHT_PERMS.additionalCharges.view, ], }; /** Both arms of a freight-type-split permission (for one-of route guards). */ export const bothFreightTypes = (p: { bulk: string; container: string; }): string[] => [p.bulk, p.container]; /** The arm of a freight-type-split permission matching a contract's freightType. */ export const forFreightType = ( p: { bulk: string; container: string }, freightType: string, ): string => (freightType === "BULK" ? p.bulk : p.container); const allRuleEngineViewKeys = () => RULE_ENGINE_RESOURCE_SLUGS.map((s) => FREIGHT_PERMS.ruleEngine.view(s)); /** * Granular equivalents of the legacy fleet:view + fleet:manage pair. * Deliberately excludes the wagon-transfer keys — those were always separate * grants (requester vs OCC vs admin history), not part of fleet:manage. */ const FLEET_GRANULAR_KEYS: string[] = [ FREIGHT_PERMS.locomotives.view, FREIGHT_PERMS.locomotives.create, FREIGHT_PERMS.locomotives.update, FREIGHT_PERMS.locomotives.delete, FREIGHT_PERMS.wagons.view, FREIGHT_PERMS.wagons.create, FREIGHT_PERMS.wagons.update, FREIGHT_PERMS.wagons.delete, FREIGHT_PERMS.wagons.statusToggle, FREIGHT_PERMS.trains.view, FREIGHT_PERMS.trains.create, FREIGHT_PERMS.trains.update, FREIGHT_PERMS.trains.delete, FREIGHT_PERMS.trains.assignWagons, FREIGHT_PERMS.trains.changeLocomotives, FREIGHT_PERMS.trains.changeYard, FREIGHT_PERMS.trains.changeWagonYard, FREIGHT_PERMS.trains.toggleActive, FREIGHT_PERMS.trains.disband, FREIGHT_PERMS.routes.view, FREIGHT_PERMS.routes.create, FREIGHT_PERMS.routes.update, FREIGHT_PERMS.routes.delete, FREIGHT_PERMS.containers.view, FREIGHT_PERMS.containers.create, FREIGHT_PERMS.containers.update, FREIGHT_PERMS.containers.delete, FREIGHT_PERMS.cargoes.view, FREIGHT_PERMS.cargoes.create, FREIGHT_PERMS.cargoes.update, FREIGHT_PERMS.cargoes.delete, FREIGHT_PERMS.consignments.view, FREIGHT_PERMS.consignments.create, ]; const allReportKeys = (): string[] => REPORT_KEYS.map((k) => reportPermissionKey(k)); // Everyone who works the booking desk also opens the overview dashboard and // the canned reports — granted alongside bookings:view in every preset below. // Each report also carries its own key (see REPORT_PERMISSIONS); spreading // allReportKeys() here keeps every existing preset seeing every report, same // as when reports:view alone gated the whole section. const STAFF_DASHBOARD_KEYS: string[] = [ FREIGHT_PERMS.overview.view, FREIGHT_PERMS.reports.view, ...allReportKeys(), ]; // Notification desks — recipient selectors, not access. A preset gets a desk // key only where it actually works that queue, which is why the GL presets take // the clearance pair and nothing else: they hold no bookings:view and no intake // keys, so intake pings would only be noise they cannot act on. const BOOKING_DESK_NOTIFICATION_KEYS: string[] = [ FREIGHT_PERMS.bookings.getNotification, FREIGHT_PERMS.contracts.getNotification, ]; const CLEARANCE_DESK_NOTIFICATION_KEYS: string[] = [ FREIGHT_PERMS.bookings.clearanceGetNotification, FREIGHT_PERMS.contracts.clearanceGetNotification, ]; export const ROLE_PERMISSION_PRESETS = { // Marketing / line staff: drives a booking from intake through line-staff // approval and contract generation/signing — i.e. until the contract is ready // and signed. No director/CEO approval, no scheduling, no operations. lineStaff: [ ...STAFF_DASHBOARD_KEYS, FREIGHT_PERMS.bookings.view, FREIGHT_PERMS.bookings.staffAccept, FREIGHT_PERMS.bookings.requestChanges, FREIGHT_PERMS.bookings.reject, FREIGHT_PERMS.bookings.approveLineStaff, FREIGHT_PERMS.bookings.rejectApproval, // Shared-wagon gate: two customers' cargo on one wagon is a commercial // call. Granted to the approver roles and NOT to GL — GL creates the // pairing, so GL approving it would defeat the second pair of eyes. FREIGHT_PERMS.bookings.approveConsolidation, FREIGHT_PERMS.bookings.cancel, FREIGHT_PERMS.bookings.wagonCancellationView, FREIGHT_PERMS.bookings.wagonCancellationVoid, FREIGHT_PERMS.contracts.view, ...bothFreightTypes(FREIGHT_PERMS.contracts.staffAccept), ...bothFreightTypes(FREIGHT_PERMS.contracts.requestChanges), ...bothFreightTypes(FREIGHT_PERMS.contracts.reject), FREIGHT_PERMS.contracts.approveLineStaff, FREIGHT_PERMS.contracts.editDocument, ...allRuleEngineViewKeys(), ...BOOKING_DESK_NOTIFICATION_KEYS, FREIGHT_PERMS.ruleEngine.getNotification, ], // Operations Officer: train scheduling + wagon allocation + transit/complete // + fleet management (wagons, trains, locomotives, routes, containers, cargo). operationsOfficer: [ ...STAFF_DASHBOARD_KEYS, FREIGHT_PERMS.bookings.view, FREIGHT_PERMS.bookings.operations, FREIGHT_PERMS.bookings.wagonCancellationView, // They are the ones who accept/reject operation requests, so they are the // ones the doc-review countdown is for. FREIGHT_PERMS.bookings.docReviewAlert, FREIGHT_PERMS.trainScheduling.view, FREIGHT_PERMS.trainScheduling.create, FREIGHT_PERMS.trainScheduling.update, FREIGHT_PERMS.trainScheduling.load, FREIGHT_PERMS.trainScheduling.unload, FREIGHT_PERMS.trainScheduling.cancel, FREIGHT_PERMS.trainScheduling.reschedule, FREIGHT_PERMS.trainScheduling.rulesManage, FREIGHT_PERMS.trainScheduling.dispatch, FREIGHT_PERMS.trainScheduling.markPaid, FREIGHT_PERMS.trainScheduling.expireBooking, FREIGHT_PERMS.trainScheduling.editTrainNumber, FREIGHT_PERMS.fleet.view, FREIGHT_PERMS.fleet.manage, ...FLEET_GRANULAR_KEYS, // Path A (no customs): Operations reviews the customer's self-clearance docs // — on the contract for ONE_TIME contracts, and PER BOOKING for GENERAL // contracts (booking-level document review → finalize → CLEARANCE_READY). FREIGHT_PERMS.contracts.view, FREIGHT_PERMS.contracts.opsClearanceReview, FREIGHT_PERMS.bookings.clearanceView, FREIGHT_PERMS.bookings.reviewDocuments, FREIGHT_PERMS.bookings.finalizeClearance, ...allRuleEngineViewKeys(), ...BOOKING_DESK_NOTIFICATION_KEYS, // They run Path A clearance review from the booking detail, so the booking // clearance desk is theirs too — but not the contract one, which is GL's. FREIGHT_PERMS.bookings.clearanceGetNotification, FREIGHT_PERMS.ruleEngine.getNotification, ], director: [ ...STAFF_DASHBOARD_KEYS, FREIGHT_PERMS.bookings.view, FREIGHT_PERMS.bookings.approveDirector, FREIGHT_PERMS.bookings.rejectApproval, // Shared-wagon gate: two customers' cargo on one wagon is a commercial // call. Granted to the approver roles and NOT to GL — GL creates the // pairing, so GL approving it would defeat the second pair of eyes. FREIGHT_PERMS.bookings.approveConsolidation, FREIGHT_PERMS.bookings.generateContract, FREIGHT_PERMS.contracts.view, FREIGHT_PERMS.contracts.approveDirector, FREIGHT_PERMS.contracts.generateContract, ...allRuleEngineViewKeys(), ...BOOKING_DESK_NOTIFICATION_KEYS, ], ceo: [ ...STAFF_DASHBOARD_KEYS, FREIGHT_PERMS.bookings.view, FREIGHT_PERMS.bookings.approveCeo, FREIGHT_PERMS.bookings.rejectApproval, // Shared-wagon gate: two customers' cargo on one wagon is a commercial // call. Granted to the approver roles and NOT to GL — GL creates the // pairing, so GL approving it would defeat the second pair of eyes. FREIGHT_PERMS.bookings.approveConsolidation, FREIGHT_PERMS.contracts.view, FREIGHT_PERMS.contracts.approveCeo, ...allRuleEngineViewKeys(), ...BOOKING_DESK_NOTIFICATION_KEYS, ], finance: [ ...STAFF_DASHBOARD_KEYS, FREIGHT_PERMS.bookings.view, FREIGHT_PERMS.invoices.view, FREIGHT_PERMS.invoices.export, // Manual settlement (bank transfer / counter) of USD and ETB invoices. FREIGHT_PERMS.invoices.confirmOffline, // Read-only: the worklist offers whichever currencies are switched on. // Flipping the switch is deliberately NOT here — see `manualPayment`. FREIGHT_PERMS.settings.manualPayment.view, // Deliberately NOT granted here: invoices:eims_register, eims_resolve, eims_cancel, // eims_receipt_register, eims:memo_issue. Automatic filing needs no human permission at all // (the cron sweep runs as the system); these are the *manual* exceptional-operations // endpoints, and stay off the general Finance role. They are granted to the `chief` position // instead — see below — the same maker–checker split already used for shipping-line credit // mark-paid/cancel (Finance raises, chief decides). FREIGHT_PERMS.payments.view, FREIGHT_PERMS.bookings.wagonCancellationView, // Shipping-line credit ledger is a Finance surface: bill batches into // invoices and RAISE manual invoice actions. Approval of those actions is // deliberately absent — it sits with the chief (maker–checker). FREIGHT_PERMS.shippingLineCredits.view, FREIGHT_PERMS.shippingLineCredits.invoice, FREIGHT_PERMS.shippingLineCredits.invoiceMarkPaid, FREIGHT_PERMS.shippingLineCredits.invoiceCancel, // Additional Payments: Finance is the only role that raises and sends // ad-hoc charges to a customer. FREIGHT_PERMS.additionalCharges.view, FREIGHT_PERMS.additionalCharges.create, FREIGHT_PERMS.additionalCharges.send, FREIGHT_PERMS.additionalCharges.cancel, ], // Global Logistics: manages ONLY the customs-clearance queue. Scoped out of // the general booking-request list (no bookings:view) — instead a dedicated // clearance:view permission lists the clearance bookings. Reviews customer // clearance documents, uploads customs output documents, and finalizes the // clearance gate. // GL Ethiopia (edr_gl_ethiopia): reviews pre-booking clearance docs on the // contract, uploads ET output docs, finalizes clearance, and EXCLUSIVELY creates // the booking under a customs contract (Path B). Also runs post-booking ET // milestones + the legacy booking-clearance permissions during migration. glEthiopia: [ FREIGHT_PERMS.contracts.view, FREIGHT_PERMS.contracts.clearanceReview, FREIGHT_PERMS.contracts.finalizeClearance, FREIGHT_PERMS.contracts.createBooking, FREIGHT_PERMS.contracts.clearanceEtActions, FREIGHT_PERMS.contracts.clearanceDutyAdvise, FREIGHT_PERMS.contracts.finalInvoiceConfirm, FREIGHT_PERMS.bookings.clearanceView, FREIGHT_PERMS.bookings.reviewDocuments, FREIGHT_PERMS.bookings.uploadClearanceOutput, FREIGHT_PERMS.bookings.finalizeClearance, FREIGHT_PERMS.bookings.operations, ...CLEARANCE_DESK_NOTIFICATION_KEYS, ], // GL Djibouti (edr_gl_djibouti): DO/RO collection, gatepass, loading milestones, // damage reports. Read-only on the contract; no booking creation. glDjibouti: [ FREIGHT_PERMS.contracts.view, FREIGHT_PERMS.contracts.clearanceDjActions, FREIGHT_PERMS.contracts.finalInvoiceRaise, FREIGHT_PERMS.contracts.finalInvoiceConfirm, FREIGHT_PERMS.bookings.clearanceView, FREIGHT_PERMS.bookings.uploadClearanceOutput, FREIGHT_PERMS.bookings.operations, ...CLEARANCE_DESK_NOTIFICATION_KEYS, ], // Marketing handles intake through contract (same as line staff here) and, // for non-customs bookings, reviews/finalizes the customer's clearance // documents from the booking detail (customs bookings go to Global Logistics). marketing: [ ...STAFF_DASHBOARD_KEYS, FREIGHT_PERMS.bookings.view, FREIGHT_PERMS.bookings.staffAccept, FREIGHT_PERMS.bookings.requestChanges, FREIGHT_PERMS.bookings.reject, FREIGHT_PERMS.bookings.approveLineStaff, FREIGHT_PERMS.bookings.rejectApproval, // Shared-wagon gate: two customers' cargo on one wagon is a commercial // call. Granted to the approver roles and NOT to GL — GL creates the // pairing, so GL approving it would defeat the second pair of eyes. FREIGHT_PERMS.bookings.approveConsolidation, FREIGHT_PERMS.bookings.cancel, FREIGHT_PERMS.bookings.wagonCancellationView, FREIGHT_PERMS.bookings.wagonCancellationVoid, FREIGHT_PERMS.bookings.wagonCancellationRebook, FREIGHT_PERMS.bookings.generateContract, FREIGHT_PERMS.bookings.signStaff, FREIGHT_PERMS.bookings.reviewDocuments, FREIGHT_PERMS.bookings.finalizeClearance, FREIGHT_PERMS.contracts.view, ...bothFreightTypes(FREIGHT_PERMS.contracts.staffAccept), ...bothFreightTypes(FREIGHT_PERMS.contracts.requestChanges), ...bothFreightTypes(FREIGHT_PERMS.contracts.reject), FREIGHT_PERMS.contracts.approveLineStaff, FREIGHT_PERMS.contracts.generateContract, ...bothFreightTypes(FREIGHT_PERMS.contracts.signStaff), FREIGHT_PERMS.contracts.suspend, FREIGHT_PERMS.contracts.editDocument, ...BOOKING_DESK_NOTIFICATION_KEYS, // Marketing follows up with the customer when a reviewer sends profile // changes back, so they sit on the customer desk: read-only on the customer // record (no verify/deactivate — the decision stays with the chief) plus the // desk key the change-request pings are addressed to. FREIGHT_PERMS.customers.view, FREIGHT_PERMS.customers.getNotification, ], orgManager: [...BOOKING_RULE_ENGINE_PERMISSION_KEYS], } as const; /** * Position permission presets (positions-as-roles). Grants flow to users via * Position → PositionPermission (NOT Role/RolePermission). Each reuses the * matching ROLE_PERMISSION_PRESETS key-array as a building block and adds the * gap-controller keys the position needs. Deduped via Set. */ const dedupe = (keys: string[]): string[] => [...new Set(keys)]; export const POSITION_PERMISSION_PRESETS = { // Chief: senior operational role — intake/line-staff approval + director // approval + scheduling/ops, plus container allocation. chief: dedupe([ ...ROLE_PERMISSION_PRESETS.lineStaff, ...ROLE_PERMISSION_PRESETS.director, ...ROLE_PERMISSION_PRESETS.operationsOfficer, FREIGHT_PERMS.allocation.manage, // Customer desk: onboarding intake lands on the chief — open the customer // list and approve/suspend a submitted profile. Deliberately NOT granted: // create, update and password reset, which stay with the customer admins. FREIGHT_PERMS.customers.view, FREIGHT_PERMS.customers.verify, FREIGHT_PERMS.customers.deactivate, // …and therefore the profile-review pings company-notifier emits. FREIGHT_PERMS.customers.getNotification, // The chief also owns the customer-facing support inbox. FREIGHT_PERMS.support.agentView, FREIGHT_PERMS.support.agentSend, // Senior commercial oversight: government expedite + the finance views. FREIGHT_PERMS.bookings.governmentExpedite, FREIGHT_PERMS.invoices.view, FREIGHT_PERMS.invoices.export, // Manual MoR EIMS actions and credit/debit memo issuance: kept off the general Finance role // (see that preset's comment) and granted here instead — the chief is already the decision // side of every other sensitive finance action (mark-paid/cancel approval below), and these // are irreversible-at-MoR or receivable-creating in the same way. FREIGHT_PERMS.invoices.eimsCancel, FREIGHT_PERMS.invoices.eimsResolve, FREIGHT_PERMS.invoices.eimsReceiptRegister, FREIGHT_PERMS.invoices.memoIssue, FREIGHT_PERMS.payments.view, // Decision side of the credit-invoice two-step: finance raises // mark-paid/cancel requests, the chief approves or rejects them. FREIGHT_PERMS.shippingLineCredits.view, FREIGHT_PERMS.shippingLineCredits.invoiceApprove, FREIGHT_PERMS.shippingLineCredits.invoiceReject, ]), // Director additionally manages train scheduling + rail fleet (same block the // operation officer/chief hold), on top of the approval-chain role preset, // and carries the same full warehouse authority the chief tier holds. director: dedupe([ ...ROLE_PERMISSION_PRESETS.director, FREIGHT_PERMS.trainScheduling.view, FREIGHT_PERMS.trainScheduling.create, FREIGHT_PERMS.trainScheduling.update, FREIGHT_PERMS.trainScheduling.load, FREIGHT_PERMS.trainScheduling.unload, FREIGHT_PERMS.trainScheduling.cancel, FREIGHT_PERMS.trainScheduling.reschedule, FREIGHT_PERMS.trainScheduling.rulesManage, FREIGHT_PERMS.trainScheduling.editTrainNumber, FREIGHT_PERMS.fleet.view, FREIGHT_PERMS.fleet.manage, ...FLEET_GRANULAR_KEYS, // Warehouse — full CRUD, matching the chief tier. Unlike the dispatcher, // the director also owns the allocation and fee rules themselves. FREIGHT_PERMS.warehouseDashboard.view, ...Object.values(FREIGHT_PERMS.warehouses), ...Object.values(FREIGHT_PERMS.warehouseYards), ...Object.values(FREIGHT_PERMS.warehouseZones), ...Object.values(FREIGHT_PERMS.warehouseAllocationRules), ...Object.values(FREIGHT_PERMS.warehouseFeeRules), ...Object.values(FREIGHT_PERMS.warehouseInventory), ...Object.values(FREIGHT_PERMS.warehouseInspectionReports), ...Object.values(FREIGHT_PERMS.interchangeDocuments), ...Object.values(FREIGHT_PERMS.warehouseFeeInvoices), ]), ceo: dedupe([...ROLE_PERMISSION_PRESETS.ceo]), ethiopianGl: dedupe([...ROLE_PERMISSION_PRESETS.glEthiopia]), djiboutiGl: dedupe([...ROLE_PERMISSION_PRESETS.glDjibouti]), marketer: dedupe([...ROLE_PERMISSION_PRESETS.marketing]), operation: dedupe([ ...ROLE_PERMISSION_PRESETS.operationsOfficer, FREIGHT_PERMS.allocation.manage, ]), // Operations Chief: full operational authority — the entire freight // permission catalog (all CRUD across bookings, contracts, scheduling, // fleet, warehouse, mile, finance, settings, staff). operationsChief: dedupe([...BOOKING_RULE_ENGINE_PERMISSION_KEYS]), // Dispatcher: full CRUD on warehouse management (incl. import/export/intercity // inventory flows) and fleet management, plus truck dispatch on the mile legs // and operational context. The ONE carve-out: allocation & fee rules stay // VIEW-ONLY — a dispatcher never creates/updates/deletes those rules. dispatcher: dedupe([ // Warehouse management — full CRUD. FREIGHT_PERMS.warehouseDashboard.view, ...Object.values(FREIGHT_PERMS.warehouses), ...Object.values(FREIGHT_PERMS.warehouseYards), ...Object.values(FREIGHT_PERMS.warehouseZones), ...Object.values(FREIGHT_PERMS.warehouseInventory), ...Object.values(FREIGHT_PERMS.warehouseInspectionReports), ...Object.values(FREIGHT_PERMS.interchangeDocuments), ...Object.values(FREIGHT_PERMS.warehouseFeeInvoices), // View-only on the rules that govern allocation and fees. FREIGHT_PERMS.warehouseAllocationRules.view, FREIGHT_PERMS.warehouseFeeRules.view, // Fleet management — full CRUD. ...Object.values(FREIGHT_PERMS.fleet), FREIGHT_PERMS.fleetDashboard.view, ...Object.values(FREIGHT_PERMS.fleetReports), ...Object.values(FREIGHT_PERMS.vehicles), ...Object.values(FREIGHT_PERMS.drivers), ...Object.values(FREIGHT_PERMS.tracking), ...Object.values(FREIGHT_PERMS.fuel), ...Object.values(FREIGHT_PERMS.maintenance), ...Object.values(FREIGHT_PERMS.locomotives), ...Object.values(FREIGHT_PERMS.wagons), ...Object.values(FREIGHT_PERMS.trains), ...Object.values(FREIGHT_PERMS.routes), ...Object.values(FREIGHT_PERMS.containers), ...Object.values(FREIGHT_PERMS.cargoes), // Truck dispatch on the EDR mile legs + operational context. ...Object.values(FREIGHT_PERMS.firstMile), ...Object.values(FREIGHT_PERMS.lastMile), FREIGHT_PERMS.trainScheduling.view, FREIGHT_PERMS.bookings.operations, // Road-fleet back office: vendors, asset lifecycle, compliance, facilities. ...Object.values(FREIGHT_PERMS.procurement), ...Object.values(FREIGHT_PERMS.compliance), ...Object.values(FREIGHT_PERMS.facilities), ]), // Truck & Machinery chief: reviews and approves/rejects last-mile // confirmation requests (the pre-approval gate ahead of vehicle assignment), // plus enough fleet visibility to judge truck availability. truckMachineryChief: dedupe([ FREIGHT_PERMS.lastMile.view, FREIGHT_PERMS.lastMile.requestView, FREIGHT_PERMS.lastMile.requestReview, FREIGHT_PERMS.lastMile.requestApprove, FREIGHT_PERMS.fleetDashboard.view, FREIGHT_PERMS.vehicles.view, ]), } as const; /** Derive the module bucket from the resource segment of a permission key. */ const moduleOf = (key: string): string => key.split(":")[1] ?? "other"; export const PERMISSIONS_CATALOG = [ ...BOOKING_RULE_ENGINE_PERMISSIONS, ...FREIGHT_READ_PERMISSIONS, ].map((p) => ({ key: p.key, label: p.name.en, module: moduleOf(p.key), }));