diff --git a/apps/edr-freight-api/src/seed/freight-permission-key-migration.seeder.ts b/apps/edr-freight-api/src/seed/freight-permission-key-migration.seeder.ts index 0a0f86a64..bdc3f9139 100644 --- a/apps/edr-freight-api/src/seed/freight-permission-key-migration.seeder.ts +++ b/apps/edr-freight-api/src/seed/freight-permission-key-migration.seeder.ts @@ -1,5 +1,10 @@ import { Injectable, Logger } from '@nestjs/common'; -import { Permission } from '@tria-plc/iamapi-common'; +import { + Permission, + PositionPermission, + PositionTypePermission, + RolePermission, +} from '@tria-plc/iamapi-common'; import { DataSource } from 'typeorm'; /** Renamed rule-engine resources: old key -> new key (same permission id). */ @@ -14,6 +19,22 @@ const PERMISSION_KEY_RENAMES: ReadonlyArray<{ from: string; to: string }> = [ }, ]; +/** + * Keys retired by the permission-system redesign (docs/permission-system/01): + * seeded but never enforced anywhere, and no matching feature exists. Grants + * referencing them are revoked before the permission row is deleted. + */ +const RETIRED_PERMISSION_KEYS: ReadonlyArray = [ + 'edr_freight_app:payments:verify', + 'edr_freight_app:payments:refund', + 'edr_freight_app:invoices:create', + 'edr_freight_app:invoices:cancel', + 'edr_freight_app:fuel:approve', + 'edr_freight_app:maintenance:complete', + 'edr_freight_app:bookings:payment_pnr', + 'edr_freight_app:bookings:payment_verify', +]; + @Injectable() export class FreightPermissionKeyMigrationSeeder { private readonly logger = new Logger(FreightPermissionKeyMigrationSeeder.name); @@ -44,5 +65,29 @@ export class FreightPermissionKeyMigrationSeeder { await permissionRepository.update({ id: existing.id }, { key: to }); this.logger.log(`Renamed permission key ${from} -> ${to}`); } + + for (const key of RETIRED_PERMISSION_KEYS) { + const existing = await permissionRepository.findOne({ + where: { key }, + select: { id: true }, + }); + if (!existing) { + continue; + } + + // Revoke every grant first, then drop the permission row itself. + const permissionId = existing.id as string; + await this.dataSource + .getRepository(RolePermission) + .delete({ permissionId }); + await this.dataSource + .getRepository(PositionPermission) + .delete({ permissionId }); + await this.dataSource + .getRepository(PositionTypePermission) + .delete({ permissionId }); + await permissionRepository.delete({ id: permissionId }); + this.logger.log(`Retired permission key ${key}`); + } } } diff --git a/apps/edr-freight-api/src/seed/freight-permissions.registry.ts b/apps/edr-freight-api/src/seed/freight-permissions.registry.ts index 11cccd782..1465bab85 100644 --- a/apps/edr-freight-api/src/seed/freight-permissions.registry.ts +++ b/apps/edr-freight-api/src/seed/freight-permissions.registry.ts @@ -52,8 +52,6 @@ export const BOOKING_PERMISSIONS: FreightPermissionSeed[] = [ 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-00000000000b', 'edr_freight_app:bookings:payment_pnr', 'Generate PNR'), - perm('a1000001-0001-4000-8000-00000000000c', 'edr_freight_app:bookings:payment_verify', 'Verify payment'), 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'), @@ -201,11 +199,7 @@ export const CUSTOMER_PERMISSIONS: FreightPermissionSeed[] = [ // D. Finance — payments + invoices export const FINANCE_PERMISSIONS: FreightPermissionSeed[] = [ perm('d2a00001-0001-4000-8000-000000000001', 'edr_freight_app:payments:view', 'View payments'), - perm('d2a00001-0001-4000-8000-000000000002', 'edr_freight_app:payments:verify', 'Verify/settle payment'), - perm('d2a00001-0001-4000-8000-000000000003', 'edr_freight_app:payments:refund', 'Refund payment'), perm('d2b00001-0001-4000-8000-000000000001', 'edr_freight_app:invoices:view', 'View invoices'), - perm('d2b00001-0001-4000-8000-000000000002', 'edr_freight_app:invoices:create', 'Generate invoice'), - perm('d2b00001-0001-4000-8000-000000000003', 'edr_freight_app:invoices:cancel', 'Cancel invoice'), perm('d2b00001-0001-4000-8000-000000000004', 'edr_freight_app:invoices:export', 'Download invoice document'), ]; @@ -293,12 +287,10 @@ export const FLEET_ROAD_PERMISSIONS: FreightPermissionSeed[] = [ 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('e2d00001-0001-4000-8000-000000000005', 'edr_freight_app:fuel:approve', 'Approve 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('e2e00001-0001-4000-8000-000000000005', 'edr_freight_app:maintenance:complete', 'Complete 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'), @@ -386,6 +378,45 @@ export const STAFF_IAM_PERMISSIONS: FreightPermissionSeed[] = [ 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'), +]; + +// 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'), +]; + export const ADVANCED_BACKOFFICE_PERMISSIONS: FreightPermissionSeed[] = [ ...CUSTOMER_PERMISSIONS, ...FINANCE_PERMISSIONS, @@ -397,6 +428,8 @@ export const ADVANCED_BACKOFFICE_PERMISSIONS: FreightPermissionSeed[] = [ ...SCHEDULING_EXTRA_PERMISSIONS, ...CONFIG_SETTINGS_PERMISSIONS, ...STAFF_IAM_PERMISSIONS, + ...AUDIENCE_GAP_PERMISSIONS, + ...GRANULAR_SPLIT_PERMISSIONS, ]; export const BOOKING_RULE_ENGINE_PERMISSIONS = [ @@ -431,6 +464,7 @@ export const FREIGHT_PERMS = { 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', }, contracts: { view: 'edr_freight_app:contracts:view', @@ -464,6 +498,9 @@ export const FREIGHT_PERMS = { 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', }, trainScheduling: { view: 'edr_freight_app:train_scheduling:view', @@ -472,6 +509,9 @@ export const FREIGHT_PERMS = { 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', }, fleet: { view: 'edr_freight_app:fleet:view', @@ -503,13 +543,9 @@ export const FREIGHT_PERMS = { }, payments: { view: 'edr_freight_app:payments:view', - verify: 'edr_freight_app:payments:verify', - refund: 'edr_freight_app:payments:refund', }, invoices: { view: 'edr_freight_app:invoices:view', - create: 'edr_freight_app:invoices:create', - cancel: 'edr_freight_app:invoices:cancel', export: 'edr_freight_app:invoices:export', }, firstMile: { @@ -621,14 +657,12 @@ export const FREIGHT_PERMS = { create: 'edr_freight_app:fuel:create', update: 'edr_freight_app:fuel:update', delete: 'edr_freight_app:fuel:delete', - approve: 'edr_freight_app:fuel:approve', }, 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', - complete: 'edr_freight_app:maintenance:complete', }, fleetReports: { view: 'edr_freight_app:fleet_reports:view', @@ -708,11 +742,50 @@ export const FREIGHT_PERMS = { view: 'edr_freight_app:settings:dropdown:view', manage: 'edr_freight_app:settings:dropdown: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', + }, }, audit: { view: 'edr_freight_app:audit:view', }, + 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', + }, staff: { + users: { + view: 'edr_freight_app:staff:users:view', + }, roles: { view: 'edr_freight_app:staff:roles:view', create: 'edr_freight_app:staff:roles:create', @@ -813,11 +886,19 @@ const FLEET_GRANULAR_KEYS: string[] = [ FREIGHT_PERMS.consignments.create, ]; +// Everyone who works the booking desk also opens the overview dashboard and +// the canned reports — granted alongside bookings:view in every preset below. +const STAFF_DASHBOARD_KEYS: string[] = [ + FREIGHT_PERMS.overview.view, + FREIGHT_PERMS.reports.view, +]; + 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, @@ -830,11 +911,13 @@ export const ROLE_PERMISSION_PRESETS = { ...bothFreightTypes(FREIGHT_PERMS.contracts.requestChanges), ...bothFreightTypes(FREIGHT_PERMS.contracts.reject), FREIGHT_PERMS.contracts.approveLineStaff, + FREIGHT_PERMS.contracts.editDocument, ...allRuleEngineViewKeys(), ], // 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, // They are the ones who accept/reject operation requests, so they are the @@ -846,6 +929,9 @@ export const ROLE_PERMISSION_PRESETS = { 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.fleet.view, FREIGHT_PERMS.fleet.manage, ...FLEET_GRANULAR_KEYS, @@ -860,6 +946,7 @@ export const ROLE_PERMISSION_PRESETS = { ...allRuleEngineViewKeys(), ], director: [ + ...STAFF_DASHBOARD_KEYS, FREIGHT_PERMS.bookings.view, FREIGHT_PERMS.bookings.approveDirector, FREIGHT_PERMS.bookings.rejectApproval, @@ -870,6 +957,7 @@ export const ROLE_PERMISSION_PRESETS = { ...allRuleEngineViewKeys(), ], ceo: [ + ...STAFF_DASHBOARD_KEYS, FREIGHT_PERMS.bookings.view, FREIGHT_PERMS.bookings.approveCeo, FREIGHT_PERMS.bookings.rejectApproval, @@ -877,7 +965,13 @@ export const ROLE_PERMISSION_PRESETS = { FREIGHT_PERMS.contracts.approveCeo, ...allRuleEngineViewKeys(), ], - finance: [FREIGHT_PERMS.bookings.view], + finance: [ + ...STAFF_DASHBOARD_KEYS, + FREIGHT_PERMS.bookings.view, + FREIGHT_PERMS.invoices.view, + FREIGHT_PERMS.invoices.export, + FREIGHT_PERMS.payments.view, + ], // 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 @@ -894,6 +988,7 @@ export const ROLE_PERMISSION_PRESETS = { 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, @@ -905,6 +1000,8 @@ export const ROLE_PERMISSION_PRESETS = { 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, @@ -913,6 +1010,7 @@ export const ROLE_PERMISSION_PRESETS = { // 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, @@ -932,6 +1030,7 @@ export const ROLE_PERMISSION_PRESETS = { FREIGHT_PERMS.contracts.generateContract, ...bothFreightTypes(FREIGHT_PERMS.contracts.signStaff), FREIGHT_PERMS.contracts.suspend, + FREIGHT_PERMS.contracts.editDocument, ], orgManager: [...BOOKING_RULE_ENGINE_PERMISSION_KEYS], } as const; @@ -958,6 +1057,14 @@ export const POSITION_PERMISSION_PRESETS = { FREIGHT_PERMS.customers.view, FREIGHT_PERMS.customers.verify, FREIGHT_PERMS.customers.deactivate, + // 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, + FREIGHT_PERMS.payments.view, ]), director: dedupe([...ROLE_PERMISSION_PRESETS.director]), ceo: dedupe([...ROLE_PERMISSION_PRESETS.ceo]), @@ -1009,6 +1116,10 @@ export const POSITION_PERMISSION_PRESETS = { ...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),