mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 10:08:21 +00:00
feat(exchange-settings): one fallback rate per currency
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
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { Body, Controller, Get, Patch } from "@nestjs/common";
|
||||
import { BadRequestException, Body, Controller, Get, Param, Patch } from "@nestjs/common";
|
||||
import { ApiBearerAuth, ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||
import { CurrentUser } from "@edr/api-common";
|
||||
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";
|
||||
@@ -8,6 +8,31 @@ 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")
|
||||
@@ -17,37 +42,51 @@ export class ExchangeSettingsController {
|
||||
@Get()
|
||||
@BookingStaff([FREIGHT_PERMS.settings.exchangeRate.view, FREIGHT_PERMS.admin])
|
||||
@ApiOperation({
|
||||
summary: "Current USD→ETB fallback rate and CBE feed health",
|
||||
summary: "Current X→ETB fallback rates and CBE feed health, one entry per currency",
|
||||
})
|
||||
async get() {
|
||||
const setting = await this.service.get();
|
||||
const status = this.service.getFeedStatus();
|
||||
async list() {
|
||||
const settings = await this.service.list();
|
||||
const byCurrency = new Map(settings.map((s) => [s.currency, s]));
|
||||
|
||||
return {
|
||||
fallbackRate: setting.fallbackRate,
|
||||
fallbackSource: setting.fallbackSource,
|
||||
lastSyncedAt: setting.lastSyncedAt,
|
||||
updatedById: setting.updatedById,
|
||||
feed: status,
|
||||
};
|
||||
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()
|
||||
@Patch(":currency")
|
||||
@BookingStaff([FREIGHT_PERMS.settings.exchangeRate.manage, FREIGHT_PERMS.admin])
|
||||
@ApiOperation({
|
||||
summary:
|
||||
"Set the USD→ETB fallback by hand (used only while CBE is unreachable)",
|
||||
"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,
|
||||
|
||||
Reference in New Issue
Block a user