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.
This commit is contained in:
Nathnael
2026-08-31 10:34:12 +00:00
parent ae3dd825ba
commit 9d48fc189a

View File

@@ -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 <booking-reference>');
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);
});