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)
// ────────────────────────────────────────────────────────────
// 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
* tables (the live odds elsewhere are only a JSON blob on live_matches). This
* is what makes opening→closing ranges and steam queryable in SQL. Mirrors the
* historical feeder's change-detection (feeder-persistence.service.ts) but for
* the live HTML odds format. Change-only writes; ALL markets in the payload
* are tracked (so anomalies/steam on any bet type are captured).
* Persist live PRE-MATCH odds movement into a dedicated, FK-FREE table
* (live_odds_history). Why not the structured odds_history table: that one is
* tied to odd_categories.match_id, a FOREIGN KEY to `matches`. Upcoming matches
* live ONLY in live_matches (not yet archived to `matches`), so any write to
* odd_categories/odds_history for them fails the FK — silently losing all
* 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(
matchId: string,
odds: Record<string, Record<string, number>>,
): Promise<void> {
try {
const cats = await this.prisma.oddCategory.findMany({
where: { matchId },
include: { selections: true },
});
for (const [catName, sels] of Object.entries(odds)) {
let category = cats.find((c) => c.name === catName);
if (!category) {
category = await this.prisma.oddCategory.create({
data: { matchId, name: catName },
include: { selections: true },
});
cats.push(category);
if (!DataFetcherTask.liveOddsTableReady) {
await this.prisma.$executeRawUnsafe(
`CREATE TABLE IF NOT EXISTS live_odds_history (
id BIGSERIAL PRIMARY KEY,
match_id TEXT NOT NULL,
market TEXT NOT NULL,
selection TEXT NOT NULL,
previous_value DOUBLE PRECISION,
new_value DOUBLE PRECISION NOT NULL,
change_time TIMESTAMPTZ NOT NULL DEFAULT now())`,
);
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);
if (!selName || !sVal || !(value > 0)) continue;
const existing = category.selections.find((s) => s.name === selName);
if (existing) {
if (existing.oddValue !== sVal) {
const oldVal = parseFloat(existing.oddValue || "0");
const newVal = parseFloat(sVal);
if (!isNaN(oldVal) && !isNaN(newVal) && oldVal > 0) {
await this.prisma.oddsHistory.create({
data: {
selectionId: existing.dbId,
// Last-known value per (market, selection) for this match.
const last = await this.prisma.$queryRawUnsafe<
Array<{ market: string; selection: string; new_value: number }>
>(
`SELECT DISTINCT ON (market, selection) market, selection, new_value
FROM live_odds_history WHERE match_id = $1
ORDER BY market, selection, change_time DESC`,
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 },
data: { oddValue: sVal },
});
existing.oddValue = sVal;
}
} else {
const created = await this.prisma.oddSelection.create({
data: {
categoryId: category.dbId,
name: selName,
oddValue: sVal,
openingValue: sVal,
},
});
category.selections.push(created);
for (const [market, sels] of Object.entries(odds)) {
for (const [selection, value] of Object.entries(sels)) {
if (!(value > 0)) continue;
const prev = lastMap.get(`${market}|${selection}`);
if (prev === undefined) {
// first capture for this selection = opening
await this.prisma.$executeRawUnsafe(
`INSERT INTO live_odds_history (match_id, market, selection, previous_value, new_value)
VALUES ($1, $2, $3, NULL, $4)`,
matchId, market, selection, value,
);
lastMap.set(`${market}|${selection}`, value);
} else if (Math.abs(prev - value) > 1e-9) {
// odds moved → log the movement
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) {
const message = err instanceof Error ? err.message : String(err);
this.logger.debug(
`odds_history persist skipped for ${matchId}: ${message}`,
// WARN (not debug): silent debug is exactly what hid the earlier FK failure.
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,
// so opening→closing ranges & steam are captured in the DB (not just the
// live_matches JSON blob). Runs only for pre-match matches reached here.
// Log this pre-match odds refresh into live_odds_history (FK-free table) so
// opening→closing movement & steam are queryable in the DB for UPCOMING
// matches too (the structured odds_history can't — FK to `matches`).
if (Object.keys(odds).length > 0) {
await this.persistOddsHistory(match.id, odds);
}