/** Admin claim reads / status changes. Changing a claim's status never rewrites an entity column: the worker's next reconciliation does. */ import type { ClaimDTO } from "@dci/core"; import { CAPACITY_COLUMN } from "@dci/core"; import { pg, claimCols, andAll, page, type Fragment } from "../../lib/sql.js"; import { int, num, reqStr, str, type Row } from "../../lib/rows.js"; import { claimDto } from "../../lib/dto.js"; export interface ClaimFilters { subjectType?: string; subjectId?: string; status?: string; predicate?: string; page?: number; perPage?: number } export async function listClaims(f: ClaimFilters): Promise<{ items: ClaimDTO[]; total: number; page: number; perPage: number }> { const sql = pg(); const pg_ = page(f.page, f.perPage, 500, 50); const c: Fragment[] = []; if (f.subjectType) c.push(sql`k.subject_type = ${f.subjectType}`); if (f.subjectId) c.push(sql`k.subject_id = ${f.subjectId}`); if (f.status && f.status !== "all") c.push(sql`k.status = ${f.status}`); if (f.predicate) c.push(sql`k.predicate = ${f.predicate}`); const rows = await sql`select ${claimCols(sql)}, count(*) over() as total from claims k left join sources s on s.id = k.source_id where ${andAll(sql, c)} order by k.last_observed desc, k.id limit ${pg_.perPage} offset ${pg_.offset}`; return { items: rows.map((r) => claimDto(r, false)), total: rows.length ? int(rows[0]!.total) : 0, page: pg_.page, perPage: pg_.perPage }; } const FACILITY_COL: Record = { itCapacityMw: "it_capacity_mw", totalPowerMw: "total_power_mw", plannedPowerMw: "planned_power_mw", utilityCapacityMw: "utility_capacity_mw", gridConnectionMw: "grid_connection_mw", ultimateCampusMw: "ultimate_campus_mw" }; /** Set a claim status; when a rejected claim was backing a displayed facility MW column, a quality flag is raised (column untouched). */ export async function setClaimStatus(id: string, status: "current" | "rejected" | "review", reason: string | null): Promise<{ claim: ClaimDTO; flagged: boolean } | null> { const sql = pg(); const rows = await sql`update claims set status = ${status}, rejection_reason = ${status === "rejected" ? reason : null} where id = ${id} returning id`; if (!rows[0]) return null; const full = (await sql`select ${claimCols(sql)} from claims k left join sources s on s.id = k.source_id where k.id = ${id}`)[0]!; let flagged = false; if (status === "rejected" && (str(full.subject_type) === "facility" || str(full.subject_type) === "campus")) { const camel = (CAPACITY_COLUMN as Record)[reqStr(full.predicate)] ?? null; const col = camel ? FACILITY_COL[camel] : null; const v = num(full.value); if (col && v != null) { const fac = (await sql`select ${sql(col)} as v, name from facilities where id = ${reqStr(full.subject_id)}`)[0]; const cur = num(fac?.v); if (fac && cur != null && Math.abs(cur - v) < 1e-9) { await sql`insert into quality_flags (id, entity_type, entity_id, claim_id, code, severity, field, message, details, priority, status, dedupe_key) values (${"flg_" + id.replace(/^clm_/, "").slice(0, 24)}, ${reqStr(full.subject_type)}, ${reqStr(full.subject_id)}, ${id}, 'claim_rejected_backing_value', 'warn', ${camel}, ${`rejected claim (${reqStr(full.predicate)} = ${v}) is still the displayed ${camel} value — re-reconcile`}, ${JSON.stringify({ claimId: id, value: v, reason })}::jsonb, 60, 'open', ${"claim_rejected:" + id}) on conflict (dedupe_key) do update set status = 'open', message = excluded.message, details = excluded.details, updated_at = now()`; flagged = true; } } } return { claim: claimDto(full, false), flagged }; }