import test from 'node:test' import assert from 'node:assert/strict' import { loadModule } from './load.mjs' const { euro, euroWhole, signedEuro, dateDe, sinceLabel, parseEuro, INVOICE_STATUS, CATEGORY_LABEL, URGENCY, AGE_GROUP_LABEL, pageOf, canInvoice, } = await loadModule('src/lib/financeFormat.js') const MINUS = '−' test('euro: de-DE mit Tausenderpunkt und Komma, Leerzeichen vor dem Euro-Zeichen', () => { assert.equal(euro(123456), '1.234,56 €') assert.equal(euro(10893), '108,93 €') assert.equal(euro(0), '0,00 €') assert.equal(euro(5), '0,05 €') assert.equal(euro(100), '1,00 €') assert.equal(euro(100000000), '1.000.000,00 €') }) test('euro: Platzhalter für fehlende Werte', () => { assert.equal(euro(null), '–') assert.equal(euro(undefined), '–') assert.equal(euro(NaN), '–') assert.equal(euro('abc'), '–') }) test('euro: negative Beträge mit Minuszeichen U+2212', () => { assert.equal(euro(-449), `${MINUS}4,49 €`) assert.equal(euro(-123456), `${MINUS}1.234,56 €`) assert.equal(euro(-5), `${MINUS}0,05 €`) assert.equal(euro(-0), '0,00 €') }) test('euroWhole: auf ganze Euro gerundet', () => { assert.equal(euroWhole(10893), '109 €') assert.equal(euroWhole(10849), '108 €') assert.equal(euroWhole(10850), '109 €') assert.equal(euroWhole(0), '0 €') assert.equal(euroWhole(123456), '1.235 €') assert.equal(euroWhole(null), '–') assert.equal(euroWhole(undefined), '–') }) test('euroWhole: negative Werte, keine „−0 €“', () => { assert.equal(euroWhole(-10893), `${MINUS}109 €`) assert.equal(euroWhole(-49), '0 €') assert.equal(euroWhole(-50), `${MINUS}1 €`) }) test('signedEuro: immer mit Vorzeichen, Minus ist U+2212', () => { assert.equal(signedEuro(16879), '+168,79 €') assert.equal(signedEuro(-449), `${MINUS}4,49 €`) assert.equal(signedEuro(-449).charAt(0), '−') assert.equal(signedEuro(123456), '+1.234,56 €') assert.equal(signedEuro(-123456), `${MINUS}1.234,56 €`) assert.equal(signedEuro(0), '0,00 €') assert.equal(signedEuro(null), '–') assert.equal(signedEuro(undefined), '–') }) test('dateDe: ISO-Datum zu TT.MM.JJJJ', () => { assert.equal(dateDe('2027-03-29'), '29.03.2027') assert.equal(dateDe('2026-01-05'), '05.01.2026') }) test('dateDe: ISO-Zeitpunkt in Berliner Zeit, Platzhalter für Ungültiges', () => { assert.equal(dateDe('2099-03-29T00:00:00.000Z'), '29.03.2099') // 23:30 UTC im Sommer = 01:30 am Folgetag in Berlin assert.equal(dateDe('2026-07-01T23:30:00.000Z'), '02.07.2026') assert.equal(dateDe(''), '–') assert.equal(dateDe(null), '–') assert.equal(dateDe(undefined), '–') assert.equal(dateDe('kein Datum'), '–') }) test('sinceLabel: gerade eben / Minuten / Stunden / Tage', () => { const now = Date.parse('2026-10-01T12:00:00.000Z') const ago = (ms) => new Date(now - ms).toISOString() assert.equal(sinceLabel(ago(0), now), 'gerade eben') assert.equal(sinceLabel(ago(59 * 1000), now), 'gerade eben') assert.equal(sinceLabel(ago(60 * 1000), now), 'vor 1 Min.') assert.equal(sinceLabel(ago(12 * 60000), now), 'vor 12 Min.') assert.equal(sinceLabel(ago(59 * 60000), now), 'vor 59 Min.') assert.equal(sinceLabel(ago(60 * 60000), now), 'vor 1 Std.') assert.equal(sinceLabel(ago(3 * 3600000), now), 'vor 3 Std.') assert.equal(sinceLabel(ago(23 * 3600000), now), 'vor 23 Std.') assert.equal(sinceLabel(ago(24 * 3600000), now), 'vor 1 Tag') assert.equal(sinceLabel(ago(2 * 86400000), now), 'vor 2 Tagen') assert.equal(sinceLabel(ago(40 * 86400000), now), 'vor 40 Tagen') }) test('sinceLabel: Zukunft, Date als now, fehlende Werte', () => { const now = Date.parse('2026-10-01T12:00:00.000Z') assert.equal(sinceLabel(new Date(now + 5 * 60000).toISOString(), now), 'gerade eben') assert.equal(sinceLabel('2026-10-01T11:48:00.000Z', new Date(now)), 'vor 12 Min.') assert.equal(sinceLabel(null, now), '–') assert.equal(sinceLabel('Quatsch', now), '–') }) test('parseEuro: Tausenderpunkte und Dezimalkomma', () => { assert.equal(parseEuro('1.234,56'), 123456) assert.equal(parseEuro('12,50'), 1250) assert.equal(parseEuro('12,5'), 1250) assert.equal(parseEuro('1234'), 123400) assert.equal(parseEuro('1234,5'), 123450) assert.equal(parseEuro('0,05'), 5) assert.equal(parseEuro('0'), 0) assert.equal(parseEuro('0,00'), 0) assert.equal(parseEuro('1.234'), 123400) assert.equal(parseEuro('1.234.567,89'), 123456789) assert.equal(parseEuro('19,99'), 1999) }) test('parseEuro: Leerraum und Euro-Zeichen werden toleriert', () => { assert.equal(parseEuro(' 12,50 '), 1250) assert.equal(parseEuro('12,50 €'), 1250) assert.equal(parseEuro('12,50€'), 1250) assert.equal(parseEuro('€ 12,50'), 1250) }) test('parseEuro: ergibt exakte Cent (keine Gleitkomma-Fehler)', () => { assert.equal(parseEuro('19,99'), 1999) assert.equal(parseEuro('0,29'), 29) assert.equal(parseEuro('1,15'), 115) assert.equal(parseEuro('4,35'), 435) assert.ok(Number.isInteger(parseEuro('1.234,56'))) }) test('parseEuro: ungültige Eingaben werfen „Ungültiger Betrag“', () => { const bad = [ '', ' ', 'abc', '12abc', '1,2,3', '12,345', '1,234', '12.50', '1.23', '1.2345', '1..234', '1.234,5,6', ',5', '.5', '12,', '1.', '-5', '-0', '+5', '−5', '1e3', '12 34', '1 234,56', '0.500', '€', '12,50 € 3', '١٢', '12', ] for (const text of bad) { assert.throws(() => parseEuro(text), { message: 'Ungültiger Betrag' }, `sollte werfen: ${JSON.stringify(text)}`) } }) test('parseEuro: Nicht-Strings und zu große Werte werfen', () => { for (const v of [null, undefined, 12.5, {}, [], true]) { assert.throws(() => parseEuro(v), { message: 'Ungültiger Betrag' }) } assert.throws(() => parseEuro('99999999999999999999'), { message: 'Ungültiger Betrag' }) }) test('Status- und Beschriftungstabellen (verbatim)', () => { assert.deepEqual(INVOICE_STATUS, { entwurf: { label: 'Entwurf', tone: 'muted' }, offen: { label: 'Offen', tone: 'info' }, ueberfaellig: { label: 'Überfällig', tone: 'warn' }, bezahlt: { label: 'Bezahlt', tone: 'ok' }, storniert: { label: 'Storniert', tone: 'muted' }, uneinbringlich: { label: 'Uneinbringlich', tone: 'danger' }, }) assert.deepEqual(CATEGORY_LABEL, { rechnung: 'Rechnung', 'rechnung-extern': 'Einnahme ohne Stripe-Rechnung', 'stripe-auszahlung': 'Stripe-Auszahlung', fixkosten: 'Fixkosten', privat: 'Privat/Umbuchung', sonstiges: 'Sonstiges', '': 'Offen', }) assert.deepEqual(URGENCY, { hoch: { label: 'Dringend', tone: 'danger' }, mittel: { label: 'Bald', tone: 'warn' }, info: { label: 'Hinweis', tone: 'muted' }, }) assert.deepEqual(AGE_GROUP_LABEL, { 'nicht-faellig': 'Nicht fällig', '1-30': '1–30 Tage', '31-60': '31–60 Tage', 'ueber-60': 'Über 60 Tage', }) }) test('pageOf: 1-basiert, 50 pro Seite, Standardgröße', () => { const list = Array.from({ length: 120 }, (_, i) => i + 1) const p1 = pageOf(list, 1) assert.equal(p1.items.length, 50) assert.equal(p1.items[0], 1) assert.equal(p1.page, 1) assert.equal(p1.pages, 3) const p3 = pageOf(list, 3) assert.deepEqual(p3.items, list.slice(100)) assert.equal(p3.items.length, 20) assert.equal(p3.page, 3) assert.equal(p3.pages, 3) }) test('pageOf: Seitenzahl wird begrenzt, eigene Größe, leere Liste', () => { const list = [1, 2, 3, 4, 5] assert.deepEqual(pageOf(list, 99, 2), { items: [5], page: 3, pages: 3 }) assert.deepEqual(pageOf(list, 0, 2), { items: [1, 2], page: 1, pages: 3 }) assert.deepEqual(pageOf(list, -4, 2), { items: [1, 2], page: 1, pages: 3 }) assert.deepEqual(pageOf(list, 'x', 2), { items: [1, 2], page: 1, pages: 3 }) assert.deepEqual(pageOf(list, 2.7, 2), { items: [3, 4], page: 2, pages: 3 }) assert.deepEqual(pageOf([], 1), { items: [], page: 1, pages: 1 }) assert.deepEqual(pageOf(null, 3), { items: [], page: 1, pages: 1 }) assert.deepEqual(pageOf(undefined), { items: [], page: 1, pages: 1 }) assert.equal(pageOf(Array.from({ length: 50 }, (_, i) => i), 1).pages, 1) assert.equal(pageOf(Array.from({ length: 51 }, (_, i) => i), 1).pages, 2) }) test('canInvoice: fester Kunde mit E-Mail ist ok', () => { assert.deepEqual(canInvoice({ customerStatus: 'customer', email: 'k@example.invalid' }), { ok: true }) // Bestandsdaten ohne Status gelten als fester Kunde (wie customerStage) assert.deepEqual(canInvoice({ email: 'k@example.invalid' }), { ok: true }) assert.deepEqual(canInvoice({ customerStatus: '', email: 'k@example.invalid' }), { ok: true }) }) test('canInvoice: Lead und Abgesagt sind noch keine festen Kunden', () => { const reason = 'Noch kein fester Kunde' assert.deepEqual(canInvoice({ customerStatus: 'lead', email: 'k@example.invalid' }), { ok: false, reason }) assert.deepEqual(canInvoice({ customerStatus: 'lost', email: 'k@example.invalid' }), { ok: false, reason }) assert.deepEqual(canInvoice({ customerStatus: 'Lead', email: 'k@example.invalid' }), { ok: false, reason }) assert.deepEqual(canInvoice({ customerStatus: ' LOST ', email: 'k@example.invalid' }), { ok: false, reason }) // Status hat Vorrang vor fehlender E-Mail assert.deepEqual(canInvoice({ customerStatus: 'lead', email: '' }), { ok: false, reason }) }) test('canInvoice: ohne E-Mail nicht möglich', () => { const reason = 'E-Mail fehlt' assert.deepEqual(canInvoice({ customerStatus: 'customer' }), { ok: false, reason }) assert.deepEqual(canInvoice({ customerStatus: 'customer', email: '' }), { ok: false, reason }) assert.deepEqual(canInvoice({ customerStatus: 'customer', email: ' ' }), { ok: false, reason }) assert.deepEqual(canInvoice({ customerStatus: 'customer', email: null }), { ok: false, reason }) assert.deepEqual(canInvoice({}), { ok: false, reason }) }) test('canInvoice: kein Kunde', () => { assert.equal(canInvoice(null).ok, false) assert.equal(canInvoice(undefined).ok, false) assert.equal(typeof canInvoice(null).reason, 'string') })