mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-02 10:23:41 +00:00
Adds DJF to the currencies a booking or shipment can be billed in, off by default: going live with it is Finance's call, not a deploy's. Schema. Every currency column in freight is already a varchar that fits 'DJF' except payments.currency, the schema's one enum-typed currency column, which would reject the value outright — so the migration adds the enum label. PG 12+ allows ADD VALUE inside a transaction (migrations run with transaction mode 'each') as long as the new label is not used in the same one, and nothing here inserts it. down() drops only the flags: Postgres cannot remove an enum label, and trying would orphan any row already written with it. Two flags, because they answer different questions. exchange_settings .djf_enabled gates whether DJF is offered at all and starts off. manual_payment_settings.djf_enabled gates bank-transfer settlement and starts on — a DJF invoice has to be settleable the day the first one is raised, which is exactly why USD has always started on. Enforcement. class-validator cannot see a database flag, so the static whitelists admit DJF and ExchangeSettingsService.assertCurrencyAllowed() decides whether it is live. It is called where a currency is chosen — booking create and update, and shipment creation under a contract — not where one is read. Only the requested currency is checked, never the resolved one, so switching DJF off stops new choices instead of bricking shipment creation on contracts already written in it. Contract creation needs no check: contracts are always quoted in USD and ignore a client-supplied currency. resolveShipmentCurrency stays pure and synchronous; a checked async wrapper sits beside it, resolved before the insert callbacks (which are sync, and re-run on a reference collision) rather than inside them. GET /exchange-settings/currencies is readable by customers as well as staff, so the portal's picker can offer exactly what the API will accept instead of a hardcoded pair that fails on submit. PATCH now leaves an omitted field alone, so flipping the toggle does not re-stamp the fallback rate as MANUAL as a side effect.
85 lines
2.8 KiB
TypeScript
85 lines
2.8 KiB
TypeScript
import { Body, Controller, Get, Patch } from "@nestjs/common";
|
|
import { ApiBearerAuth, ApiOperation, ApiTags } from "@nestjs/swagger";
|
|
import { CurrentUser } from "@edr/api-common";
|
|
import type { TCurrentUser } from "@tria-plc/api-common/modules/auth/types/current-user.type";
|
|
|
|
import { BookingStaff, MixedAudience } 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";
|
|
|
|
@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 USD→ETB fallback rate and CBE feed health",
|
|
})
|
|
async get() {
|
|
const setting = await this.service.get();
|
|
const status = this.service.getFeedStatus();
|
|
|
|
return {
|
|
fallbackRate: setting.fallbackRate,
|
|
fallbackSource: setting.fallbackSource,
|
|
lastSyncedAt: setting.lastSyncedAt,
|
|
updatedById: setting.updatedById,
|
|
djfEnabled: setting.djfEnabled,
|
|
feed: status,
|
|
};
|
|
}
|
|
|
|
@Get("currencies")
|
|
@MixedAudience([FREIGHT_PERMS.settings.exchangeRate.view, FREIGHT_PERMS.admin])
|
|
@ApiOperation({
|
|
summary: "Currencies a booking or contract may be billed in right now",
|
|
})
|
|
async currencies() {
|
|
// Customers reach this too — the portal's currency picker must offer exactly what the
|
|
// API will accept, or the choice fires and comes back a 400.
|
|
return { currencies: await this.service.enabledCurrencies() };
|
|
}
|
|
|
|
@Patch()
|
|
@BookingStaff([FREIGHT_PERMS.settings.exchangeRate.manage, FREIGHT_PERMS.admin])
|
|
@ApiOperation({
|
|
summary:
|
|
"Set the USD→ETB fallback by hand (used only while CBE is unreachable) " +
|
|
"and/or turn DJF billing on and off",
|
|
})
|
|
async update(
|
|
@Body() dto: UpdateExchangeSettingDto,
|
|
@CurrentUser() user: TCurrentUser,
|
|
) {
|
|
// An omitted field leaves that setting alone — setting the rate stamps it MANUAL, and
|
|
// toggling DJF must not do that as a side effect.
|
|
let updated = await this.service.get();
|
|
|
|
if (dto.fallbackRate !== undefined) {
|
|
updated = await this.service.setManualRate(
|
|
dto.fallbackRate,
|
|
user?.id ?? null,
|
|
);
|
|
}
|
|
|
|
if (dto.djfEnabled !== undefined) {
|
|
updated = await this.service.setDjfEnabled(
|
|
dto.djfEnabled,
|
|
user?.id ?? null,
|
|
);
|
|
}
|
|
|
|
return {
|
|
fallbackRate: updated.fallbackRate,
|
|
fallbackSource: updated.fallbackSource,
|
|
lastSyncedAt: updated.lastSyncedAt,
|
|
updatedById: updated.updatedById,
|
|
djfEnabled: updated.djfEnabled,
|
|
};
|
|
}
|
|
}
|