From 9d48fc189abbba4999e6180da401a94d6c75be3a Mon Sep 17 00:00:00 2001 From: Nathnael Date: Mon, 31 Aug 2026 10:34:12 +0000 Subject: [PATCH] chore(scripts): price one booking in every currency to check DJF Exercising DJF through the portal means enabling the flag, building a booking, and clicking through the wizard before a single franc figure appears. This prices an existing booking three ways instead and prints the lines side by side, so the rate feed, the conversion, the zero-decimal rounding and the cross-currency parity are all visible in one command. Read-only: computePriceForBooking persists nothing and the currency is flipped on the in-memory entity, so it is safe against a shared database and safe to re-run. pnpm --filter @edr/freight-api exec ts-node -r tsconfig-paths/register \ src/scripts/check-djf-pricing.ts BK-2026-000221 The parity line is the check worth reading: the same freight quoted in ETB and in DJF must come back to the same money once converted. Anything past rounding drift means a rate or a conversion is wrong. --- .../src/scripts/check-djf-pricing.ts | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 apps/edr-freight-api/src/scripts/check-djf-pricing.ts diff --git a/apps/edr-freight-api/src/scripts/check-djf-pricing.ts b/apps/edr-freight-api/src/scripts/check-djf-pricing.ts new file mode 100644 index 000000000..c06135336 --- /dev/null +++ b/apps/edr-freight-api/src/scripts/check-djf-pricing.ts @@ -0,0 +1,94 @@ +import 'reflect-metadata'; +import { config } from 'dotenv'; +import { resolve } from 'path'; + +config({ path: resolve(__dirname, '../../.env') }); + +import { NestFactory } from '@nestjs/core'; +import { ExchangeService } from '@edr/api-common'; + +import { AppModule } from '../app.module'; +import { BookingPricingService } from '../modules/bookings/booking-pricing.service'; +import { BookingsService } from '../modules/bookings/bookings.service'; + +/** + * Prices one existing booking in each currency and prints the totals side by side. + * + * READ-ONLY. computePriceForBooking() persists nothing, and the booking's currency is + * flipped on the in-memory entity only — nothing is saved, so this is safe to run against + * a shared database and safe to re-run. + * + * Usage: + * pnpm --filter @edr/freight-api exec ts-node -r tsconfig-paths/register \ + * src/scripts/check-djf-pricing.ts BK-2026-000221 + */ +async function main() { + const reference = process.argv[2]; + if (!reference) { + console.error('Usage: check-djf-pricing.ts '); + process.exit(1); + } + + const app = await NestFactory.createApplicationContext(AppModule, { + logger: ['error', 'warn'], + }); + + try { + const exchange = app.get(ExchangeService, { strict: false }); + const pricing = app.get(BookingPricingService, { strict: false }); + const bookings = app.get(BookingsService, { strict: false }); + + const usdToEtb = await exchange.getRate('USD', 'ETB'); + const usdToDjf = await exchange.getRate('USD', 'DJF'); + const djfToEtb = await exchange.getRate('DJF', 'ETB'); + const feed = exchange.getProviderStatus(); + + console.log('\nRates'); + console.log(` source ${feed.source ?? 'unknown'}${feed.lastError ? ` (${feed.lastError})` : ''}`); + console.log(` USD → ETB ${usdToEtb}`); + console.log(` USD → DJF ${usdToDjf.toFixed(4)} (peg is 177.7210)`); + console.log(` DJF → ETB ${djfToEtb.toFixed(6)}`); + + const booking = await bookings.findByReference(reference); + console.log(`\nBooking ${booking.reference} — ${booking.tradeDirection} ${booking.freightType}, stored in ${booking.paymentCurrency}`); + + for (const currency of ['USD', 'ETB', 'DJF'] as const) { + // In-memory only. Never saved. + const priced = await pricing.computePriceForBooking({ + ...booking, + paymentCurrency: currency, + } as typeof booking); + + const decimals = currency === 'DJF' ? 0 : 2; + console.log(`\n ${currency} total ${priced.totalAmount.toFixed(decimals)}`); + for (const line of priced.lineItems) { + console.log( + ` ${line.code.padEnd(28)} ${String(line.amount.toFixed(decimals)).padStart(14)} ${line.currency}`, + ); + } + const fractional = priced.lineItems.filter( + (l) => currency === 'DJF' && !Number.isInteger(l.amount), + ); + if (fractional.length > 0) { + console.log(` ⚠ ${fractional.length} DJF line(s) carry centimes — DJF is zero-decimal`); + } + } + + // The cross-check that matters: the same freight, quoted three ways, is one price. + const etb = (await pricing.computePriceForBooking({ ...booking, paymentCurrency: 'ETB' } as typeof booking)).totalAmount; + const djf = (await pricing.computePriceForBooking({ ...booking, paymentCurrency: 'DJF' } as typeof booking)).totalAmount; + const djfAsEtb = djf * djfToEtb; + const driftPct = Math.abs(djfAsEtb - etb) / etb * 100; + console.log( + `\nParity ${djf.toFixed(0)} DJF = ${djfAsEtb.toFixed(2)} ETB vs ${etb.toFixed(2)} ETB priced directly` + + ` → ${driftPct.toFixed(3)}% drift (rounding only; > 0.5% means something is wrong)`, + ); + } finally { + await app.close(); + } +} + +main().catch((err) => { + console.error(err); + process.exit(1); +});