Merge pull request #1113 from Tria-plc/freight_feature/usermanagement

Freight feature/usermanagement
This commit is contained in:
marshal
2026-08-04 20:31:16 +03:00
committed by GitHub
34 changed files with 1322 additions and 243 deletions

View File

@@ -1,6 +1,6 @@
import { Body, Controller, Get, Patch } from "@nestjs/common";
import { ApiBearerAuth, ApiOperation, ApiTags } from "@nestjs/swagger";
import { CurrentUser, ExchangeService } from "@edr/api-common";
import { CurrentUser } from "@edr/api-common";
import type { TCurrentUser } from "@tria-plc/api-common/modules/auth/types/current-user.type";
import { FreightAdmin } from "../../common/booking-guards";
@@ -11,10 +11,7 @@ import { ExchangeSettingsService } from "./exchange-settings.service";
@ApiBearerAuth()
@Controller("exchange-settings")
export class ExchangeSettingsController {
constructor(
private readonly service: ExchangeSettingsService,
private readonly exchangeService: ExchangeService,
) {}
constructor(private readonly service: ExchangeSettingsService) {}
@Get()
@FreightAdmin()
@@ -22,24 +19,15 @@ export class ExchangeSettingsController {
summary: "Current USD→ETB fallback rate and CBE feed health",
})
async get() {
const [setting, status] = [
await this.service.get(),
this.exchangeService.getProviderStatus(),
];
const setting = await this.service.get();
const status = this.service.getFeedStatus();
return {
fallbackRate: setting.fallbackRate,
fallbackSource: setting.fallbackSource,
lastSyncedAt: setting.lastSyncedAt,
updatedById: setting.updatedById,
feed: {
rate: status.rate,
source: status.source,
lastSuccessAt: status.lastSuccessAt
? new Date(status.lastSuccessAt).toISOString()
: null,
lastError: status.lastError,
},
feed: status,
};
}

View File

@@ -10,6 +10,18 @@ import { ExchangeSetting } from "./entities/exchange-setting.entity";
*/
const SEED_FALLBACK_RATE = 162.4165;
/** Health of the CBE feed, as surfaced to the backoffice. */
export interface ExchangeFeedStatus {
/** Rate most recently observed, whatever its source. */
rate: number | null;
/** `live` means CBE answered; `stored`/`default` mean it is failing. */
source: "live" | "stored" | null;
/** ISO timestamp of the last successful fetch. */
lastSuccessAt: string | null;
/** Message from the most recent failure, cleared on success. */
lastError: string | null;
}
/**
* Owns the single `exchange_settings` row: the USD→ETB fallback used when the
* CBE endpoint is unreachable.
@@ -22,11 +34,30 @@ const SEED_FALLBACK_RATE = 162.4165;
export class ExchangeSettingsService {
private readonly logger = new Logger(ExchangeSettingsService.name);
/**
* Feed health, recorded from the exchange provider's callbacks rather than
* read off an injected `ExchangeService`. The provider is registered several
* times (bookings, contracts, warehouses), so no single instance sees every
* fetch — and injecting one here would be circular, since those
* registrations inject *this* service.
*/
private feed: ExchangeFeedStatus = {
rate: null,
source: null,
lastSuccessAt: null,
lastError: null,
};
constructor(
@InjectRepository(ExchangeSetting)
private readonly repository: Repository<ExchangeSetting>,
) {}
/** Health of the CBE feed as last observed by any provider instance. */
getFeedStatus(): ExchangeFeedStatus {
return { ...this.feed };
}
/** The settings row, created at the seed rate on first access. */
async get(): Promise<ExchangeSetting> {
const existing = await this.repository.findOne({ where: {} });
@@ -47,15 +78,22 @@ export class ExchangeSettingsService {
* than propagating a database error into a pricing call.
*/
async loadFallbackRate(): Promise<number | null> {
// Only reached when the live fetch failed, so this call is itself the
// signal that the feed is down.
try {
const { fallbackRate } = await this.get();
return Number.isFinite(fallbackRate) && fallbackRate > 0
? fallbackRate
: null;
const usable = Number.isFinite(fallbackRate) && fallbackRate > 0;
this.feed = {
...this.feed,
rate: usable ? fallbackRate : this.feed.rate,
source: "stored",
lastError: this.feed.lastError ?? "CBE endpoint unreachable",
};
return usable ? fallbackRate : null;
} catch (err) {
this.logger.warn(
`Could not read stored exchange fallback: ${(err as Error).message}`,
);
const message = (err as Error).message;
this.feed = { ...this.feed, source: "stored", lastError: message };
this.logger.warn(`Could not read stored exchange fallback: ${message}`);
return null;
}
}
@@ -66,6 +104,14 @@ export class ExchangeSettingsService {
* down, so a working CBE feed takes precedence again.
*/
async saveFallbackRate(rate: number): Promise<void> {
// Only called after a successful fetch, so the feed is confirmed healthy.
this.feed = {
rate,
source: "live",
lastSuccessAt: new Date().toISOString(),
lastError: null,
};
const current = await this.get();
await this.repository.update(current.id, {
fallbackRate: rate,