Update data-fetcher.task.ts
Deploy Iddaai Backend / build-and-deploy (push) Successful in 58s

This commit is contained in:
2026-06-07 21:28:52 +03:00
parent 7b17aa1fee
commit 42b6c7ce43
+67 -55
View File
@@ -1249,73 +1249,85 @@ export class DataFetcherTask {
// (Preserved from original — no logic changes) // (Preserved from original — no logic changes)
// ──────────────────────────────────────────────────────────── // ────────────────────────────────────────────────────────────
// One-time guard: run CREATE TABLE IF NOT EXISTS only once per process.
private static liveOddsTableReady = false;
/** /**
* Persist live odds movement into the structured odd_selections / odds_history * Persist live PRE-MATCH odds movement into a dedicated, FK-FREE table
* tables (the live odds elsewhere are only a JSON blob on live_matches). This * (live_odds_history). Why not the structured odds_history table: that one is
* is what makes opening→closing ranges and steam queryable in SQL. Mirrors the * tied to odd_categories.match_id, a FOREIGN KEY to `matches`. Upcoming matches
* historical feeder's change-detection (feeder-persistence.service.ts) but for * live ONLY in live_matches (not yet archived to `matches`), so any write to
* the live HTML odds format. Change-only writes; ALL markets in the payload * odd_categories/odds_history for them fails the FK — silently losing all
* are tracked (so anomalies/steam on any bet type are captured). * pre-match movement. This table is keyed by raw match_id (no FK), so it
* captures opening→closing movement for upcoming matches across ALL markets.
* Change-only inserts; first capture per selection = opening (previous NULL).
* (Finished-match closing odds are still captured in odd_selections/odds_history
* by the archival / 08:00 job, which DO have the match in `matches`.)
*/ */
private async persistOddsHistory( private async persistOddsHistory(
matchId: string, matchId: string,
odds: Record<string, Record<string, number>>, odds: Record<string, Record<string, number>>,
): Promise<void> { ): Promise<void> {
try { try {
const cats = await this.prisma.oddCategory.findMany({ if (!DataFetcherTask.liveOddsTableReady) {
where: { matchId }, await this.prisma.$executeRawUnsafe(
include: { selections: true }, `CREATE TABLE IF NOT EXISTS live_odds_history (
}); id BIGSERIAL PRIMARY KEY,
for (const [catName, sels] of Object.entries(odds)) { match_id TEXT NOT NULL,
let category = cats.find((c) => c.name === catName); market TEXT NOT NULL,
if (!category) { selection TEXT NOT NULL,
category = await this.prisma.oddCategory.create({ previous_value DOUBLE PRECISION,
data: { matchId, name: catName }, new_value DOUBLE PRECISION NOT NULL,
include: { selections: true }, change_time TIMESTAMPTZ NOT NULL DEFAULT now())`,
}); );
cats.push(category); await this.prisma.$executeRawUnsafe(
`CREATE INDEX IF NOT EXISTS idx_loh_match_time ON live_odds_history(match_id, change_time)`,
);
DataFetcherTask.liveOddsTableReady = true;
} }
for (const [selName, value] of Object.entries(sels)) {
const sVal = String(value); // Last-known value per (market, selection) for this match.
if (!selName || !sVal || !(value > 0)) continue; const last = await this.prisma.$queryRawUnsafe<
const existing = category.selections.find((s) => s.name === selName); Array<{ market: string; selection: string; new_value: number }>
if (existing) { >(
if (existing.oddValue !== sVal) { `SELECT DISTINCT ON (market, selection) market, selection, new_value
const oldVal = parseFloat(existing.oddValue || "0"); FROM live_odds_history WHERE match_id = $1
const newVal = parseFloat(sVal); ORDER BY market, selection, change_time DESC`,
if (!isNaN(oldVal) && !isNaN(newVal) && oldVal > 0) {
await this.prisma.oddsHistory.create({
data: {
selectionId: existing.dbId,
matchId, matchId,
previousValue: oldVal, );
newValue: newVal, const lastMap = new Map<string, number>();
}, for (const r of last) {
}); lastMap.set(`${r.market}|${r.selection}`, Number(r.new_value));
} }
await this.prisma.oddSelection.update({
where: { dbId: existing.dbId }, for (const [market, sels] of Object.entries(odds)) {
data: { oddValue: sVal }, for (const [selection, value] of Object.entries(sels)) {
}); if (!(value > 0)) continue;
existing.oddValue = sVal; const prev = lastMap.get(`${market}|${selection}`);
} if (prev === undefined) {
} else { // first capture for this selection = opening
const created = await this.prisma.oddSelection.create({ await this.prisma.$executeRawUnsafe(
data: { `INSERT INTO live_odds_history (match_id, market, selection, previous_value, new_value)
categoryId: category.dbId, VALUES ($1, $2, $3, NULL, $4)`,
name: selName, matchId, market, selection, value,
oddValue: sVal, );
openingValue: sVal, lastMap.set(`${market}|${selection}`, value);
}, } else if (Math.abs(prev - value) > 1e-9) {
}); // odds moved → log the movement
category.selections.push(created); await this.prisma.$executeRawUnsafe(
`INSERT INTO live_odds_history (match_id, market, selection, previous_value, new_value)
VALUES ($1, $2, $3, $4, $5)`,
matchId, market, selection, prev, value,
);
lastMap.set(`${market}|${selection}`, value);
} }
} }
} }
} catch (err: unknown) { } catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err); const message = err instanceof Error ? err.message : String(err);
this.logger.debug( // WARN (not debug): silent debug is exactly what hid the earlier FK failure.
`odds_history persist skipped for ${matchId}: ${message}`, this.logger.warn(
`live_odds_history persist failed for ${matchId}: ${message}`,
); );
} }
} }
@@ -1488,9 +1500,9 @@ export class DataFetcherTask {
}, },
}); });
// Fill the structured odds_history table from this pre-match odds refresh, // Log this pre-match odds refresh into live_odds_history (FK-free table) so
// so opening→closing ranges & steam are captured in the DB (not just the // opening→closing movement & steam are queryable in the DB for UPCOMING
// live_matches JSON blob). Runs only for pre-match matches reached here. // matches too (the structured odds_history can't — FK to `matches`).
if (Object.keys(odds).length > 0) { if (Object.keys(odds).length > 0) {
await this.persistOddsHistory(match.id, odds); await this.persistOddsHistory(match.id, odds);
} }