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.
This commit is contained in:
Hagernesh
2026-08-06 08:01:52 +00:00
parent b4ac92e14e
commit 69f6fd36a9
7 changed files with 39 additions and 7 deletions

View File

@@ -460,8 +460,21 @@ export class LastMileService {
@OnEvent("last_mile.invoice.paid")
async onBookingInvoicePaid(payload: InvoiceEventPayload): Promise<void> {
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) {

View File

@@ -43,6 +43,12 @@ export class YardsRepository implements IYardsRepository {
findPaged(query: ListYardsQueryDto): Promise<PaginatedResponse<Yard>> {
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');

View File

@@ -234,7 +234,7 @@ const RuleEngineCardGrid = ({
{col.header}:
</Text>
<div style={{ textAlign: "right", flex: 1 }}>
{formatCell(displayValue, col.format)}
{formatCell(displayValue, col.format, record)}
</div>
</Group>
);

View File

@@ -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") {

View File

@@ -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<string, unknown>,
): ReactNode => {
if (value === null || value === undefined || value === "") {
return <Text size="sm" c="dimmed"></Text>;
}
@@ -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 (
<Text size="sm" fw={500}>
{Number.isNaN(num) ? String(value) : `USD ${num.toLocaleString()}`}
{Number.isNaN(num) ? String(value) : `${code} ${num.toLocaleString()}`}
</Text>
);
}

View File

@@ -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;

View File

@@ -70,6 +70,8 @@ export interface FormFieldDef {
* relation list (`wagonTypeIds` read from `record.wagonTypes`).
*/
getInitialValue?: (record: Record<string, unknown>) => 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