Files
edr-platform/apps/edr-freight-api/src/seed/freight-permissions.registry.ts

2673 lines
93 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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",
] 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,
});
/**
* One entry per report definition (see modules/reports/definitions). Each
* gets its own permission, gated behind the `reports:view` master key that
* opens the Reports section itself.
* Keep new keys at the END: reportPermId derives ids from list index, so a
* mid-list insert would shift ids already seeded for later keys.
*/
export const 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",
] as const;
export type ReportKey = (typeof REPORT_KEYS)[number];
export const reportPermissionKey = (key: ReportKey): 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(" ");
export const REPORT_PERMISSIONS: FreightPermissionSeed[] = REPORT_KEYS.map(
(key, index) =>
perm(reportPermId(index), reportPermissionKey(key), `Report: ${titleCase(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",
),
];
/**
* Contract-phase permissions (contractbooking 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<RuleEngineResourceSlug, string> = {
"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",
};
// 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<RuleEngineResourceSlug, string>
> = {
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",
),
];
/**
* 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(
"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'. 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",
),
];
// 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(
"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",
),
];
export const ADVANCED_BACKOFFICE_PERMISSIONS: FreightPermissionSeed[] = [
...REPORT_PERMISSIONS,
...CUSTOMER_PERMISSIONS,
...SHIPPING_LINE_PERMISSIONS,
...CHAT_PERMISSIONS,
...FINANCE_PERMISSIONS,
...MILE_PERMISSIONS,
...FLEET_RAIL_PERMISSIONS,
...FLEET_ROAD_PERMISSIONS,
...WAREHOUSE_PERMISSIONS,
...PORT_TERMINAL_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,
...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. `<module>: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",
// 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",
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",
},
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",
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",
},
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",
},
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",
},
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 `<module>: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<string, string[]> = {
[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,
],
};
/** 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.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,
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.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,
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,
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,
// 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 makerchecker 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 (makerchecker).
FREIGHT_PERMS.shippingLineCredits.view,
FREIGHT_PERMS.shippingLineCredits.invoice,
FREIGHT_PERMS.shippingLineCredits.invoiceMarkPaid,
FREIGHT_PERMS.shippingLineCredits.invoiceCancel,
],
// 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,
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.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),
}));