From 69f6fd36a94724515e705992fad0529173e14502 Mon Sep 17 00:00:00 2001 From: Hagernesh Date: Thu, 6 Aug 2026 08:01:52 +0000 Subject: [PATCH] fix(rates): render stored rate currency, default last mile to birr The rate matrix currency cell hardcoded USD, so a last-mile rate priced in ETB still displayed as dollars. formatCell now takes the row and reads its currency code, falling back to USD. Last-mile currency select gets defaultValue ETB (new generic FormFieldDef.defaultValue for create-time pre-selection) and lists ETB (Birr) first; USD stays selectable. Backend already persisted and validated the chosen currency. --- .../src/modules/last-mile/last-mile.service.ts | 17 +++++++++++++++-- .../repositories/yards.repository.ts | 6 ++++++ .../ruleEngine/RuleEngineCardGrid.tsx | 2 +- .../ruleEngine/RuleEngineFormDialog.tsx | 2 ++ .../components/ruleEngine/ruleEngineFormat.tsx | 11 +++++++++-- .../pages/ruleEngine/RuleEngineResourcePage.tsx | 2 +- .../src/pages/ruleEngine/config/resources.ts | 6 +++++- 7 files changed, 39 insertions(+), 7 deletions(-) diff --git a/apps/edr-freight-api/src/modules/last-mile/last-mile.service.ts b/apps/edr-freight-api/src/modules/last-mile/last-mile.service.ts index ec7d4ccf6..8ed5ae8aa 100644 --- a/apps/edr-freight-api/src/modules/last-mile/last-mile.service.ts +++ b/apps/edr-freight-api/src/modules/last-mile/last-mile.service.ts @@ -460,8 +460,21 @@ export class LastMileService { @OnEvent("last_mile.invoice.paid") async onBookingInvoicePaid(payload: InvoiceEventPayload): Promise { try { - // Invoice paid → the delivery is complete. Route through update() so it - // also frees the trucks + records history (same as "Mark Delivered"). + if (payload.type === 'LAST_MILE_ADVANCE') { + // Advance paid → the leg becomes dispatchable, not delivered. + await this.update(payload.sourceId, { + status: 'READY_TO_TRANSIT', + advancedPayment: payload.totalAmount, + } as unknown as UpdateLastMileDto); + this.logger.log( + `Last-mile ${payload.sourceId} READY_TO_TRANSIT on advance invoice ${payload.invoiceId} payment`, + ); + return; + } + if (payload.type !== 'DELIVERY_FEE') return; + // Delivery-fee invoice paid → the delivery is complete. Route through + // update() so it also frees the trucks + records history (same as + // "Mark Delivered"). await this.update(payload.sourceId, { status: 'DELIVERED', paid: true } as unknown as UpdateLastMileDto); this.logger.log(`Last-mile ${payload.sourceId} marked DELIVERED on invoice ${payload.invoiceId} payment`); } catch (err) { diff --git a/apps/edr-freight-api/src/modules/rule-engine/repositories/yards.repository.ts b/apps/edr-freight-api/src/modules/rule-engine/repositories/yards.repository.ts index 5db5b72ae..b99b33a29 100644 --- a/apps/edr-freight-api/src/modules/rule-engine/repositories/yards.repository.ts +++ b/apps/edr-freight-api/src/modules/rule-engine/repositories/yards.repository.ts @@ -43,6 +43,12 @@ export class YardsRepository implements IYardsRepository { findPaged(query: ListYardsQueryDto): Promise> { const qb = this.repo .createQueryBuilder('yard') + // createQueryBuilder does NOT auto-apply the soft-delete filter that + // repo.find()/findOne() get for free — without this, a renamed/replaced + // yard (e.g. an old "DMP" superseded by a new one) still shows up + // alongside the live one in every picker built off this endpoint, and a + // route picked against the dead yard id never matches any LIVE rate. + .where('yard.deleted_at IS NULL') .orderBy(`yard.${query.sortBy ?? 'displayOrder'}`, query.sortOrder ?? 'ASC') .addOrderBy('yard.label', 'ASC'); diff --git a/apps/edr-freight-web/backoffice/src/components/ruleEngine/RuleEngineCardGrid.tsx b/apps/edr-freight-web/backoffice/src/components/ruleEngine/RuleEngineCardGrid.tsx index 392bf6dfb..762f5bc84 100644 --- a/apps/edr-freight-web/backoffice/src/components/ruleEngine/RuleEngineCardGrid.tsx +++ b/apps/edr-freight-web/backoffice/src/components/ruleEngine/RuleEngineCardGrid.tsx @@ -234,7 +234,7 @@ const RuleEngineCardGrid = ({ {col.header}:
- {formatCell(displayValue, col.format)} + {formatCell(displayValue, col.format, record)}
); diff --git a/apps/edr-freight-web/backoffice/src/components/ruleEngine/RuleEngineFormDialog.tsx b/apps/edr-freight-web/backoffice/src/components/ruleEngine/RuleEngineFormDialog.tsx index d205aae26..4684a3ad1 100644 --- a/apps/edr-freight-web/backoffice/src/components/ruleEngine/RuleEngineFormDialog.tsx +++ b/apps/edr-freight-web/backoffice/src/components/ruleEngine/RuleEngineFormDialog.tsx @@ -127,6 +127,8 @@ const buildInitialValues = ( } else { values[field.name] = raw; } + } else if (field.defaultValue !== undefined) { + values[field.name] = field.defaultValue; } else if (field.type === "boolean") { values[field.name] = false; } else if (field.type === "number") { diff --git a/apps/edr-freight-web/backoffice/src/components/ruleEngine/ruleEngineFormat.tsx b/apps/edr-freight-web/backoffice/src/components/ruleEngine/ruleEngineFormat.tsx index 173c75b5f..529f53a43 100644 --- a/apps/edr-freight-web/backoffice/src/components/ruleEngine/ruleEngineFormat.tsx +++ b/apps/edr-freight-web/backoffice/src/components/ruleEngine/ruleEngineFormat.tsx @@ -17,7 +17,13 @@ const extractLabel = (value: unknown): string | null => { ); }; -export const formatCell = (value: unknown, format?: ColumnFormat): ReactNode => { +export const formatCell = ( + value: unknown, + format?: ColumnFormat, + // The row the cell came from — currency amounts read their code off it so a + // last-mile rate priced in birr does not render as USD. + row?: Record, +): ReactNode => { if (value === null || value === undefined || value === "") { return ; } @@ -109,9 +115,10 @@ export const formatCell = (value: unknown, format?: ColumnFormat): ReactNode => if (format === "currency") { const num = Number(value); + const code = typeof row?.currency === "string" ? row.currency : "USD"; return ( - {Number.isNaN(num) ? String(value) : `USD ${num.toLocaleString()}`} + {Number.isNaN(num) ? String(value) : `${code} ${num.toLocaleString()}`} ); } diff --git a/apps/edr-freight-web/backoffice/src/pages/ruleEngine/RuleEngineResourcePage.tsx b/apps/edr-freight-web/backoffice/src/pages/ruleEngine/RuleEngineResourcePage.tsx index 3a0a36ce4..5d1d45889 100644 --- a/apps/edr-freight-web/backoffice/src/pages/ruleEngine/RuleEngineResourcePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/ruleEngine/RuleEngineResourcePage.tsx @@ -501,7 +501,7 @@ const RuleEngineResourcePage = () => { header: col.header, meta: { headerClassName, cellClassName }, cell: ({ row }) => { - const cell = formatCell(row.original[col.accessorKey], col.format); + const cell = formatCell(row.original[col.accessorKey], col.format, row.original); // On the rate column, show the proposed value under the live one — the // live value stays the headline because it is what still gets charged. if (!isRates || col.accessorKey !== "rateValue") return cell; diff --git a/apps/edr-freight-web/backoffice/src/pages/ruleEngine/config/resources.ts b/apps/edr-freight-web/backoffice/src/pages/ruleEngine/config/resources.ts index a51b542c2..a674a21eb 100644 --- a/apps/edr-freight-web/backoffice/src/pages/ruleEngine/config/resources.ts +++ b/apps/edr-freight-web/backoffice/src/pages/ruleEngine/config/resources.ts @@ -70,6 +70,8 @@ export interface FormFieldDef { * relation list (`wagonTypeIds` read from `record.wagonTypes`). */ getInitialValue?: (record: Record) => unknown; + /** Pre-selected value on create (no record yet) — e.g. last-mile currency = ETB. */ + defaultValue?: string; /** * Fully derived field: its value is computed from the live form values on * every render and the input is locked. Used for the priority-rule min @@ -297,8 +299,8 @@ export const rateUnitOptions = ( }; const CURRENCIES = [ + { label: "ETB (Birr)", value: "ETB" }, { label: "USD", value: "USD" }, - { label: "ETB", value: "ETB" }, ]; const PRIORITY_CONFIG_TYPES = [ @@ -1020,6 +1022,8 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [ required: true, options: CURRENCIES, showWhen: { field: "appliesTo", equals: ["LAST_MILE"] }, + // Birr is the norm for domestic trucking; USD stays selectable. + defaultValue: "ETB", getInitialValue: (record) => String(record.currency ?? "ETB"), }, // ── Distance tiers (create only — the page swaps this for the single