diff --git a/scripts/setup-legal.mjs b/scripts/setup-legal.mjs index 76f657d..5bd6c05 100644 --- a/scripts/setup-legal.mjs +++ b/scripts/setup-legal.mjs @@ -34,6 +34,7 @@ export const COLLECTIONS = [ { key: "liveUrl", type: "string", size: 500 }, { key: "sort", type: "integer", min: 0, max: 1000 }, ], + indexes: [{ key: "idx_sort", type: "key", attributes: ["sort"] }], }, { id: "legalVersions", @@ -49,7 +50,10 @@ export const COLLECTIONS = [ { key: "fileIds", type: "string[]", size: 64 }, { key: "source", type: "string", size: 300 }, ], - indexes: [{ key: "idx_documentKey", type: "key", attributes: ["documentKey"] }], + indexes: [ + { key: "idx_documentKey", type: "key", attributes: ["documentKey"] }, + { key: "idx_source", type: "key", attributes: ["source"] }, + ], }, { id: "providerContracts", @@ -70,6 +74,7 @@ export const COLLECTIONS = [ { key: "note", type: "string", size: 5000 }, { key: "fileIds", type: "string[]", size: 64 }, ], + indexes: [{ key: "idx_provider", type: "key", attributes: ["provider"] }], }, ]; @@ -92,6 +97,21 @@ export const DOCUMENTS = [ const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); +/** Wartet, bis Appwrite alle Attribute einer Sammlung fertig angelegt hat. */ +export async function waitForAttributes(db, collectionId, { intervalMs = 1000, timeoutMs = 120000 } = {}) { + const until = Date.now() + timeoutMs; + for (;;) { + const { attributes } = await db.listAttributes(databaseId, collectionId); + const failed = attributes.filter((a) => a.status === "failed" || a.status === "stuck"); + if (failed.length) { + throw new Error(`Attribut fehlgeschlagen in ${collectionId}: ${failed.map((a) => `${a.key} (${a.error || a.status})`).join(", ")}`); + } + if (attributes.every((a) => a.status === "available")) return; + if (Date.now() > until) throw new Error(`Attribute in ${collectionId} nach ${timeoutMs} ms nicht verfuegbar`); + await sleep(intervalMs); + } +} + async function createAttribute(db, collectionId, a) { const req = Boolean(a.required); switch (a.type) { @@ -143,10 +163,10 @@ async function run() { console.log(" Attribut", a.key); await sleep(400); } + await waitForAttributes(db, c.id); const indexes = new Set((await db.listIndexes(databaseId, c.id)).indexes.map((i) => i.key)); for (const idx of c.indexes || []) { if (indexes.has(idx.key)) continue; - await sleep(1500); await db.createIndex(databaseId, c.id, idx.key, idx.type, idx.attributes); console.log(" Index", idx.key); } @@ -161,7 +181,6 @@ async function run() { console.log("Bucket angelegt:", BUCKET.id); } - await sleep(2000); for (const d of DOCUMENTS) { try { await db.getDocument(databaseId, "legalDocuments", d.key); diff --git a/src/lib/legal.js b/src/lib/legal.js index 29c2ca4..e305aed 100644 --- a/src/lib/legal.js +++ b/src/lib/legal.js @@ -64,8 +64,10 @@ export function nextCancellationDate(contract, today) { if (!isSet(contract.noticeValue) || !isSet(contract.noticeUnit)) { return { state: 'incomplete', periodEnd: null, noticeBy: null } } - while (noticeDate(periodEnd, contract) < today) { - periodEnd = addMonthsClamped(periodEnd, renewal) + // Jede Periode vom Beginn aus rechnen: vom schon geklemmten Ende weiterzuzaehlen + // wuerde nach einem Februar auf dem 28. haengen bleiben. + for (let k = 1; noticeDate(periodEnd, contract) < today; k++) { + periodEnd = addMonthsClamped(contract.startDate, term + k * renewal) } return { state: 'active', periodEnd, noticeBy: noticeDate(periodEnd, contract) } } diff --git a/tests/legal.test.mjs b/tests/legal.test.mjs index 292492a..7183a23 100644 --- a/tests/legal.test.mjs +++ b/tests/legal.test.mjs @@ -136,3 +136,12 @@ test('Abloesung am selben Tag: validTo nie vor validFrom', () => { const r = planNewVersion([v5], { documentKey: 'agb', version: 'v6', status: 'current', validFrom: '2026-05-01' }) assert.deepEqual(r.updates, [{ id: 'v5', patch: { status: 'superseded', validTo: '2026-05-01' } }]) }) + +test('Verlaengerungen ab dem 31. driften nicht auf den 28.', () => { + const r = nextCancellationDate( + { startDate: '2025-01-31', termMonths: 1, renewalMonths: 1, noticeValue: 1, noticeUnit: 'days' }, + '2025-04-05' + ) + assert.equal(r.periodEnd, '2025-04-30') + assert.equal(r.noticeBy, '2025-04-29') +}) diff --git a/tests/setup-legal.test.mjs b/tests/setup-legal.test.mjs new file mode 100644 index 0000000..c52d711 --- /dev/null +++ b/tests/setup-legal.test.mjs @@ -0,0 +1,30 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { COLLECTIONS, waitForAttributes } from '../scripts/setup-legal.mjs' + +const indexOn = (id, attr) => + COLLECTIONS.find((c) => c.id === id).indexes?.some((i) => i.attributes.includes(attr)) + +test('Indizes fuer alle Abfragen (equal/orderAsc)', () => { + assert.ok(indexOn('legalVersions', 'documentKey')) + assert.ok(indexOn('legalVersions', 'source')) + assert.ok(indexOn('legalDocuments', 'sort')) + assert.ok(indexOn('providerContracts', 'provider')) +}) + +test('waitForAttributes wartet, bis alle Attribute verfuegbar sind', async () => { + let calls = 0 + const db = { + listAttributes: async () => { + calls++ + return { attributes: [{ key: 'a', status: 'available' }, { key: 'b', status: calls < 3 ? 'processing' : 'available' }] } + }, + } + await waitForAttributes(db, 'x', { intervalMs: 1, timeoutMs: 1000 }) + assert.equal(calls, 3) +}) + +test('waitForAttributes bricht bei fehlgeschlagenem Attribut ab', async () => { + const db = { listAttributes: async () => ({ attributes: [{ key: 'b', status: 'failed', error: 'kaputt' }] }) } + await assert.rejects(waitForAttributes(db, 'x', { intervalMs: 1, timeoutMs: 1000 }), /b/) +})