From e3df5383d15af1b8369d342935593a70cd25f301 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 14:11:52 +0000 Subject: [PATCH] AB-Kern: Nummernvergabe AB-JJJJ-NNNN nach Berliner Jahr Co-Authored-By: Claude Sonnet 5.5 --- server/services/auftraege/nummern.js | 22 ++++++++++++ test/auftraegeNummern.test.js | 54 ++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 server/services/auftraege/nummern.js create mode 100644 test/auftraegeNummern.test.js diff --git a/server/services/auftraege/nummern.js b/server/services/auftraege/nummern.js new file mode 100644 index 0000000..01ca120 --- /dev/null +++ b/server/services/auftraege/nummern.js @@ -0,0 +1,22 @@ +/** + * Nummernvergabe der Auftragsbestätigungen: AB-JJJJ-NNNN, lückenlos je Berliner Kalenderjahr (Übersicht V4, L3). + * vergibNummer schreibt nichts und sperrt nicht selbst: der Aufrufer hält SPERRE_NUMMER (konstanten.js), und die Nummer + * ist erst mit dem Speichern der AB vergeben. Eine Nummer, die schon in Gebrauch ist, wird übersprungen. + */ +import { berlinDate } from '../finance/dates.js' +import { findAuftragByNummer, listAuftraege } from './auftragStore.js' + +export function formatNummer(jahr, laufnummer) { + if (!Number.isInteger(jahr) || jahr < 2000 || jahr > 9999 || !Number.isInteger(laufnummer) || laufnummer < 1 || laufnummer > 9999) { + throw new RangeError('Ungültige Auftragsnummer.') + } + return `AB-${jahr}-${String(laufnummer).padStart(4, '0')}` +} + +export async function vergibNummer(jetzt = new Date()) { + const jahr = Number(berlinDate(jetzt).slice(0, 4)) + const vorhandene = await listAuftraege({ jahr }) + let laufnummer = vorhandene.reduce((groesste, a) => Math.max(groesste, Number.isSafeInteger(a.laufnummer) ? a.laufnummer : 0), 0) + 1 + while (await findAuftragByNummer(formatNummer(jahr, laufnummer))) laufnummer += 1 + return { nummer: formatNummer(jahr, laufnummer), jahr, laufnummer } +} diff --git a/test/auftraegeNummern.test.js b/test/auftraegeNummern.test.js new file mode 100644 index 0000000..b185de3 --- /dev/null +++ b/test/auftraegeNummern.test.js @@ -0,0 +1,54 @@ +import { test, after, beforeEach } from 'node:test' +import assert from 'node:assert/strict' +import { starteUmgebung, entwurfsDaten } from './helpers/auftraegeAb.js' + +const umg = await starteUmgebung() +after(() => umg.schliesse()) +beforeEach(umg.zuruecksetzen) +const { createAuftrag, updateAuftrag } = await import('../server/services/auftraege/auftragStore.js') +const { formatNummer, vergibNummer } = await import('../server/services/auftraege/nummern.js') + +/** Eine versendete AB mit Nummer anlegen (über den Speicher von L0). */ +async function legeNummerierteAn(jahr, laufnummer, patch = {}) { + const entwurf = await createAuftrag(entwurfsDaten()) + return updateAuftrag(entwurf.id, { status: 'versendet', nummer: formatNummer(jahr, laufnummer), jahr, laufnummer, ...patch }) +} + +test('formatNummer: AB-JJJJ-NNNN mit vier Ziffern; ungültige Werte werfen RangeError', () => { + assert.equal(formatNummer(2026, 1), 'AB-2026-0001') + assert.equal(formatNummer(2026, 42), 'AB-2026-0042') + assert.equal(formatNummer(2027, 9999), 'AB-2027-9999') + for (const [jahr, lauf] of [[2026, 0], [2026, 10000], [2026, 1.5], [2026, '1'], [1999, 1], ['2026', 1], [2026, -3]]) { + assert.throws(() => formatNummer(jahr, lauf), RangeError, `${jahr}/${lauf}`) + } +}) + +test('vergibNummer: die erste Nummer des Jahres ist 0001', async () => { + assert.deepEqual(await vergibNummer(new Date('2026-10-06T10:00:00Z')), { nummer: 'AB-2026-0001', jahr: 2026, laufnummer: 1 }) +}) + +test('vergibNummer: größte Laufnummer des Jahres plus 1 – Lücken egal, andere Jahre und Entwürfe zählen nicht', async () => { + for (const nr of [1, 2, 7]) await legeNummerierteAn(2026, nr) + await legeNummerierteAn(2025, 40) + await createAuftrag(entwurfsDaten()) // Entwurf ohne Nummer + assert.deepEqual(await vergibNummer(new Date('2026-10-06T10:00:00Z')), { nummer: 'AB-2026-0008', jahr: 2026, laufnummer: 8 }) + assert.deepEqual(await vergibNummer(new Date('2025-03-01T10:00:00Z')), { nummer: 'AB-2025-0041', jahr: 2025, laufnummer: 41 }) +}) + +test('vergibNummer: Jahreswechsel nach Berliner Zeit (31.12. 23:00 UTC ist in Berlin schon der 01.01.)', async () => { + for (const nr of [1, 2, 3]) await legeNummerierteAn(2026, nr) + assert.equal((await vergibNummer(new Date('2026-12-31T22:59:59Z'))).nummer, 'AB-2026-0004') + assert.deepEqual(await vergibNummer(new Date('2026-12-31T23:00:00Z')), { nummer: 'AB-2027-0001', jahr: 2027, laufnummer: 1 }) + assert.equal((await vergibNummer(new Date('2027-01-01T08:00:00Z'))).nummer, 'AB-2027-0001') +}) + +test('vergibNummer: zählt auch bei mehr als 25 ABs im Jahr richtig (Seitengröße 25 wie bei Appwrite ohne limit)', async () => { + for (let i = 1; i <= 30; i += 1) await legeNummerierteAn(2026, i) + assert.equal((await vergibNummer(new Date('2026-10-06T10:00:00Z'))).nummer, 'AB-2026-0031') +}) + +test('vergibNummer: überspringt eine Nummer, die es schon gibt (Dokument mit nummer, aber ohne laufnummer)', async () => { + const entwurf = await createAuftrag(entwurfsDaten()) + await updateAuftrag(entwurf.id, { status: 'versendet', nummer: 'AB-2026-0001' }) + assert.deepEqual(await vergibNummer(new Date('2026-10-06T10:00:00Z')), { nummer: 'AB-2026-0002', jahr: 2026, laufnummer: 2 }) +})