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 = { 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, }; } }