mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-06 05:23:38 +00:00
exchange_settings was a single row holding the USD->ETB fallback only. Restructured to one row per foreign currency (adds a currency column, migration 3850000000000) so DJF gets its own fallback rate, source and sync timestamp instead of a parallel column. Service/controller/DTO follow: get/loadFallbackRate/saveFallbackRate/setManualRate all take a currency now, GET /exchange-settings returns the list, and PATCH /exchange-settings/:currency sets one. Per-currency manual-rate ceiling (USD ~10,000, DJF ~100) replaces the old fixed bound. Adds a spec exercising the multi-currency CBE parse and the USD<->DJF pivot against a fixture payload. Claude-Session: https://claude.ai/code/session_01CZy77vCWhka3pnmVF9NDkL
97 lines
3.3 KiB
TypeScript
97 lines
3.3 KiB
TypeScript
import { BadRequestException, Body, Controller, Get, Param, Patch } from "@nestjs/common";
|
|
import { ApiBearerAuth, ApiOperation, ApiTags } from "@nestjs/swagger";
|
|
import { CURRENCY_CODES, CurrencyCode, CurrentUser } from "@edr/api-common";
|
|
import type { TCurrentUser } from "@tria-plc/api-common/modules/auth/types/current-user.type";
|
|
|
|
import { BookingStaff } from "../../common/booking-guards";
|
|
import { FREIGHT_PERMS } from "../../seed/freight-permissions.registry";
|
|
import { UpdateExchangeSettingDto } from "./dto/update-exchange-setting.dto";
|
|
import { ExchangeSettingsService } from "./exchange-settings.service";
|
|
|
|
/**
|
|
* Sane manual-rate ceiling per currency — bounded well outside any plausible
|
|
* published rate but far short of a fat-fingered magnitude error. USD trades
|
|
* in the hundreds (ETB per USD); DJF trades under 2 (ETB per DJF, since DJF
|
|
* itself is worth roughly 1/177th of a USD).
|
|
*/
|
|
const RATE_BOUNDS: Record<CurrencyCode, number> = {
|
|
ETB: 1,
|
|
USD: 10_000,
|
|
DJF: 100,
|
|
};
|
|
|
|
const FOREIGN_CURRENCIES = CURRENCY_CODES.filter((c) => c !== "ETB");
|
|
|
|
function assertSupportedCurrency(currency: string): (typeof FOREIGN_CURRENCIES)[number] {
|
|
const code = currency?.toUpperCase();
|
|
const match = FOREIGN_CURRENCIES.find((c) => c === code);
|
|
if (!match) {
|
|
throw new BadRequestException(
|
|
`Unsupported currency "${currency}" — must be one of ${FOREIGN_CURRENCIES.join(", ")}`,
|
|
);
|
|
}
|
|
return match;
|
|
}
|
|
|
|
@ApiTags("exchange-settings")
|
|
@ApiBearerAuth()
|
|
@Controller("exchange-settings")
|
|
export class ExchangeSettingsController {
|
|
constructor(private readonly service: ExchangeSettingsService) {}
|
|
|
|
@Get()
|
|
@BookingStaff([FREIGHT_PERMS.settings.exchangeRate.view, FREIGHT_PERMS.admin])
|
|
@ApiOperation({
|
|
summary: "Current X→ETB fallback rates and CBE feed health, one entry per currency",
|
|
})
|
|
async list() {
|
|
const settings = await this.service.list();
|
|
const byCurrency = new Map(settings.map((s) => [s.currency, s]));
|
|
|
|
return FOREIGN_CURRENCIES.map((code) => {
|
|
const setting = byCurrency.get(code);
|
|
return {
|
|
currency: code,
|
|
fallbackRate: setting?.fallbackRate ?? null,
|
|
fallbackSource: setting?.fallbackSource ?? null,
|
|
lastSyncedAt: setting?.lastSyncedAt ?? null,
|
|
updatedById: setting?.updatedById ?? null,
|
|
feed: this.service.getFeedStatus(code),
|
|
};
|
|
});
|
|
}
|
|
|
|
@Patch(":currency")
|
|
@BookingStaff([FREIGHT_PERMS.settings.exchangeRate.manage, FREIGHT_PERMS.admin])
|
|
@ApiOperation({
|
|
summary:
|
|
"Set a currency's X→ETB fallback by hand (used only while CBE is unreachable)",
|
|
})
|
|
async update(
|
|
@Param("currency") currency: string,
|
|
@Body() dto: UpdateExchangeSettingDto,
|
|
@CurrentUser() user: TCurrentUser,
|
|
) {
|
|
const code = assertSupportedCurrency(currency);
|
|
if (dto.fallbackRate > RATE_BOUNDS[code]) {
|
|
throw new BadRequestException(
|
|
`Fallback rate ${dto.fallbackRate} is outside the accepted range for ${code} (max ${RATE_BOUNDS[code]})`,
|
|
);
|
|
}
|
|
|
|
const updated = await this.service.setManualRate(
|
|
code,
|
|
dto.fallbackRate,
|
|
user?.id ?? null,
|
|
);
|
|
|
|
return {
|
|
currency: updated.currency,
|
|
fallbackRate: updated.fallbackRate,
|
|
fallbackSource: updated.fallbackSource,
|
|
lastSyncedAt: updated.lastSyncedAt,
|
|
updatedById: updated.updatedById,
|
|
};
|
|
}
|
|
}
|