import type { Db } from "../db.js"; import { nowIso, tx } from "../db.js"; import { audit } from "./audit.js"; /** * Pula kodów EAN wydawanych wariantom. FIFO — kolejny wolny kod to * najniższe id ze statusem 'available'. Przydział zapisuje też audit. */ export interface EanPoolRow { id: number; ean: string; status: "available" | "assigned"; assigned_variant_id: string | null; assigned_at: string | null; } /** Doładowanie puli. Duplikaty EAN są pomijane (UNIQUE + OR IGNORE). */ export function addEans(db: Db, eans: string[]): { added: number; skipped: number } { let added = 0; let skipped = 0; tx(db, () => { const ins = db.prepare("INSERT OR IGNORE INTO ean_pool (ean) VALUES (?)"); for (const ean of eans) { const normalized = ean.trim(); if (!normalized || !/^\d{13}$/.test(normalized)) { skipped++; continue; } if (ins.run(normalized).changes > 0) added++; else skipped++; } }); return { added, skipped }; } /** * Przydziela wariantowi kolejny wolny EAN (FIFO po id). Wywoływane * wewnątrz transakcji tworzącej wariant — przydział jest atomowy * z INSERT-em wariantu. Zwraca przydzielony kod albo null, gdy pula pusta. */ export function claimNextEan(db: Db, variantId: string, actor = "system"): string | null { return tx(db, () => { const row = db .prepare("SELECT * FROM ean_pool WHERE status = 'available' ORDER BY id LIMIT 1") .get() as unknown as EanPoolRow | undefined; if (!row) return null; db.prepare( "UPDATE ean_pool SET status = 'assigned', assigned_variant_id = ?, assigned_at = ? WHERE id = ?" ).run(variantId, nowIso(), row.id); audit(db, { actor, action: "ean.assigned", entity: "variants", entityId: variantId, detail: { ean: row.ean, poolId: row.id }, }); return row.ean; }); } /** Zwraca EAN-y wskazanych wariantów do puli (np. przy usuwaniu wariantów). */ export function releaseEansForVariants(db: Db, variantIds: string[], actor = "system"): number { if (!variantIds.length) return 0; const released = Number( db .prepare( `UPDATE ean_pool SET status = 'available', assigned_variant_id = NULL, assigned_at = NULL WHERE assigned_variant_id IN (SELECT value FROM json_each(?))` ) .run(JSON.stringify(variantIds)).changes ); if (released > 0) { audit(db, { actor, action: "ean.released", entity: "variants", entityId: variantIds[0]!, detail: { variantIds, released }, }); } return released; } export function eanForVariant(db: Db, variantId: string): string | null { const row = db .prepare("SELECT ean FROM ean_pool WHERE assigned_variant_id = ?") .get(variantId) as unknown as { ean: string } | undefined; return row?.ean ?? null; } /** * Backfill: przydziela kolejne wolne EAN-y wariantom, które powstały * zanim pula została zasilona (albo gdy pula była pusta). Kolejność * wariantów = kolejność utworzenia; FIFO puli po id — jak przy tworzeniu. */ export function backfillVariantEans(db: Db, productId?: string, actor = "system"): number { const missing = db .prepare( `SELECT v.id FROM variants v LEFT JOIN ean_pool e ON e.assigned_variant_id = v.id WHERE e.id IS NULL ${productId ? "AND v.product_id = ?" : ""} ORDER BY v.created_at, v.id` ) .all(...(productId ? [productId] : [])) as unknown as Array<{ id: string }>; let assigned = 0; for (const v of missing) { if (claimNextEan(db, v.id, actor)) assigned++; else break; // pula wyczerpana } return assigned; }