implement exchange settings management and fallback rate handling

This commit is contained in:
Marshal
2026-08-04 11:22:47 +00:00
parent 7c78a815eb
commit 56697d8fc5
22 changed files with 805 additions and 85 deletions

View File

@@ -0,0 +1,95 @@
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;
/**
* 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);
constructor(
@InjectRepository(ExchangeSetting)
private readonly repository: Repository<ExchangeSetting>,
) {}
/** 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> {
try {
const { fallbackRate } = await this.get();
return Number.isFinite(fallbackRate) && fallbackRate > 0
? fallbackRate
: null;
} catch (err) {
this.logger.warn(
`Could not read stored exchange fallback: ${(err as Error).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> {
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();
}
}