mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 00:50:56 +00:00
- Updated ContractDocumentViewModelBuilder to include cargoTypeName, containerType, and cargoSummary in the schedule. - Modified contract dynamic template tests to validate the new cargo fields. - Enhanced contract renderer service tests to reflect changes in cargo data structure. - Updated contract view model interface to include new cargo-related fields. - Improved dynamic template rendering to display cargo type and container type. - Refactored exchange settings controller and service to streamline error handling and feed status management. - Introduced article HTML conversion functions to support Quill editor integration for structured article editing. - Added tests for article HTML conversion to ensure correct round-trip processing of clauses and bullets.
142 lines
4.7 KiB
TypeScript
142 lines
4.7 KiB
TypeScript
import { Injectable, Logger } from "@nestjs/common";
|
|
import { InjectRepository } from "@nestjs/typeorm";
|
|
import { Repository } from "typeorm";
|
|
|
|
import { ExchangeSetting } from "./entities/exchange-setting.entity";
|
|
|
|
/**
|
|
* Rate used before the row exists and before the first successful CBE fetch —
|
|
* the CBE USD transactional selling rate on 2026-08-04.
|
|
*/
|
|
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.
|
|
*
|
|
* The live CBE rate is always preferred. This value is only read on failure,
|
|
* and every successful fetch overwrites it, so it tracks the last known good
|
|
* rate rather than drifting into a stale constant.
|
|
*/
|
|
@Injectable()
|
|
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: {} });
|
|
if (existing) return existing;
|
|
|
|
return this.repository.save(
|
|
this.repository.create({
|
|
fallbackRate: SEED_FALLBACK_RATE,
|
|
fallbackSource: "AUTO",
|
|
lastSyncedAt: null,
|
|
}),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Reads the stored fallback for the exchange provider. Returns `null` on any
|
|
* failure so the provider falls through to its own static default rather
|
|
* 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();
|
|
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) {
|
|
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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Records a freshly fetched live rate as the new fallback. Marked `AUTO`,
|
|
* overwriting a manual entry — a manual rate is a stopgap for while CBE is
|
|
* 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,
|
|
fallbackSource: "AUTO",
|
|
lastSyncedAt: new Date(),
|
|
updatedById: null,
|
|
});
|
|
this.logger.log(`Exchange fallback synced from CBE: ${rate} ETB/USD`);
|
|
}
|
|
|
|
/** Operator sets the fallback by hand, e.g. during a prolonged CBE outage. */
|
|
async setManualRate(
|
|
rate: number,
|
|
updatedById?: string | null,
|
|
): Promise<ExchangeSetting> {
|
|
const current = await this.get();
|
|
await this.repository.update(current.id, {
|
|
fallbackRate: rate,
|
|
fallbackSource: "MANUAL",
|
|
updatedById: updatedById ?? null,
|
|
});
|
|
this.logger.warn(
|
|
`Exchange fallback set manually to ${rate} ETB/USD by ${updatedById ?? "unknown user"}`,
|
|
);
|
|
return this.get();
|
|
}
|
|
}
|