feat(260907-efh): automatische Fingerabdruck-Nachrechnung beim Start
Nach der Formelaenderung (v1 -> v2) traegt jede Bestandszeile einen Hash der alten Formel und wuerde nie wieder auf einen neuen treffen. Die Produktionsdatenbank wird von Hand nicht angefasst, deshalb rechnet ein neuer Dienst beim Start alle veralteten Zeilen automatisch nach — an der Versionsmarke aus tender-fingerprint.ts erkannt, gleiche Bauform wie die LDAP-Bind-Passwort-Nachverschluesselung. - TenderFingerprintBackfillService: OnApplicationBootstrap, seitenweise (500 Zeilen), pro Seite eine Transaktion, Fehler werden geloggt und geschluckt statt geworfen - In tenders.module.ts VOR TenderSchedulerService eingetragen, damit die Nachrechnung vor der Cron-Registrierung laeuft - Vier Tests: leere DB, alter+leerer Hash werden beide erfasst, zweiter Lauf schreibt nichts mehr, Datenbankfehler wirft den Haken nicht Gemessen an der lokalen Datenbank (16.255 Zeilen): Fingerabdruck-Gruppen mit mehr als einer Zeile vorher=0, nachher=26, veraltete Zeilen danach=0, davon Gruppen mit widersprechenden Wert-Groessenordnungen=2. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K5jtbGzC5Sf9npJ3JCjKhq
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { TenderFingerprintBackfillService } from './tender-fingerprint-backfill.service';
|
||||
import { tenderFingerprint } from './tender-fingerprint';
|
||||
|
||||
/**
|
||||
* TenderFingerprintBackfillService.spec — WINDOWS.md #11 Task 3. Hand-
|
||||
* rolled fake PrismaService (Map), matching the project convention
|
||||
* (tender-dedup.service.spec.ts, ldap-config.service.spec.ts). No real DB
|
||||
* I/O — the real-DB recompute is proven separately by the plan's verify
|
||||
* step against the local database.
|
||||
*/
|
||||
|
||||
function isOutdated(fingerprint: string | null): boolean {
|
||||
return fingerprint === null || !fingerprint.startsWith('v2:');
|
||||
}
|
||||
|
||||
function makeFakePrisma(seedRows: any[]) {
|
||||
const tenders = new Map<string, any>(seedRows.map((r) => [r.id, { ...r }]));
|
||||
|
||||
const findManyOutdated = vi.fn(async ({ select, take }: any) => {
|
||||
const rows = [...tenders.values()].filter((t) => isOutdated(t.fingerprint));
|
||||
const page = rows.slice(0, take);
|
||||
return page.map((row) => {
|
||||
const out: any = {};
|
||||
for (const k of Object.keys(select)) out[k] = row[k];
|
||||
return out;
|
||||
});
|
||||
});
|
||||
|
||||
const update = vi.fn(async ({ where, data }: any) => {
|
||||
const existing = tenders.get(where.id);
|
||||
const updated = { ...existing, ...data };
|
||||
tenders.set(where.id, updated);
|
||||
return updated;
|
||||
});
|
||||
|
||||
const transaction = vi.fn(async (ops: Promise<unknown>[]) => Promise.all(ops));
|
||||
|
||||
const prisma = {
|
||||
tender: { findMany: findManyOutdated, update },
|
||||
$transaction: transaction,
|
||||
__store: { tenders },
|
||||
};
|
||||
|
||||
return prisma;
|
||||
}
|
||||
|
||||
describe('TenderFingerprintBackfillService', () => {
|
||||
it('eine Datenbank ohne Zeilen loest keine Schreiboperation aus und der Haken wirft nicht', async () => {
|
||||
const prisma = makeFakePrisma([]);
|
||||
const service = new TenderFingerprintBackfillService(prisma as any);
|
||||
|
||||
await expect(service.onApplicationBootstrap()).resolves.not.toThrow();
|
||||
expect(prisma.tender.update).not.toHaveBeenCalled();
|
||||
expect(prisma.$transaction).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Zeilen mit altem Hash und Zeilen mit leerem Hash werden beide erfasst und tragen danach die neue Marke', async () => {
|
||||
const prisma = makeFakePrisma([
|
||||
{
|
||||
id: 't-old',
|
||||
buyerName: 'Stadt Altstadt',
|
||||
title: 'Sanierung Rathaus',
|
||||
deadlineAt: null,
|
||||
fingerprint: 'oldhashvalue',
|
||||
},
|
||||
{
|
||||
id: 't-null',
|
||||
buyerName: 'Stadt Neustadt',
|
||||
title: 'Neubau Schule',
|
||||
deadlineAt: null,
|
||||
fingerprint: null,
|
||||
},
|
||||
{
|
||||
id: 't-current',
|
||||
buyerName: 'Stadt Gegenwart',
|
||||
title: 'Bereits aktuell',
|
||||
deadlineAt: null,
|
||||
fingerprint: 'v2:already-current',
|
||||
},
|
||||
]);
|
||||
const service = new TenderFingerprintBackfillService(prisma as any);
|
||||
|
||||
const updated = await service.backfillOutdated();
|
||||
|
||||
expect(updated).toBe(2);
|
||||
const rowOld = prisma.__store.tenders.get('t-old');
|
||||
const rowNull = prisma.__store.tenders.get('t-null');
|
||||
const rowCurrent = prisma.__store.tenders.get('t-current');
|
||||
expect(rowOld.fingerprint).toBe(
|
||||
tenderFingerprint({ buyerName: 'Stadt Altstadt', title: 'Sanierung Rathaus', deadlineAt: null }),
|
||||
);
|
||||
expect(rowNull.fingerprint).toBe(
|
||||
tenderFingerprint({ buyerName: 'Stadt Neustadt', title: 'Neubau Schule', deadlineAt: null }),
|
||||
);
|
||||
// Unangetastet — trug bereits die neue Marke.
|
||||
expect(rowCurrent.fingerprint).toBe('v2:already-current');
|
||||
});
|
||||
|
||||
it('ein zweiter Lauf direkt danach schreibt nichts mehr', async () => {
|
||||
const prisma = makeFakePrisma([
|
||||
{
|
||||
id: 't-old',
|
||||
buyerName: 'Stadt Altstadt',
|
||||
title: 'Sanierung Rathaus',
|
||||
deadlineAt: null,
|
||||
fingerprint: 'oldhashvalue',
|
||||
},
|
||||
]);
|
||||
const service = new TenderFingerprintBackfillService(prisma as any);
|
||||
|
||||
const first = await service.backfillOutdated();
|
||||
expect(first).toBe(1);
|
||||
|
||||
prisma.tender.update.mockClear();
|
||||
const second = await service.backfillOutdated();
|
||||
|
||||
expect(second).toBe(0);
|
||||
expect(prisma.tender.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('wirft die Datenbankabfrage, wirft der Haken trotzdem nicht', async () => {
|
||||
const prisma = {
|
||||
tender: {
|
||||
findMany: vi.fn().mockRejectedValue(new Error('DB weg')),
|
||||
update: vi.fn(),
|
||||
},
|
||||
$transaction: vi.fn(),
|
||||
};
|
||||
const service = new TenderFingerprintBackfillService(prisma as any);
|
||||
|
||||
await expect(service.onApplicationBootstrap()).resolves.not.toThrow();
|
||||
expect(prisma.tender.update).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user