Rechnungen: Positionen-Grundlagen (Stundentext, Kalenderwoche, feste Positionen, Prüfung)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8627ed1124
commit
7553253746
2 files changed
+206
No files matched your search
@@ -0,0 +1,93 @@
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import {
|
||||
stundenText, leistungszeitraumText, kalenderwoche, gueltigerTag, kuerzen,
|
||||
festePositionen, zusatzPositionen, pruefePositionen,
|
||||
} from '../server/services/auftraege/positionen.js'
|
||||
|
||||
/** Wirft die Funktion einen AuftragError mit diesem Status und dieser Meldung? */
|
||||
const wirft = (fn, status, message) =>
|
||||
assert.throws(fn, (err) => {
|
||||
assert.equal(err.status, status, `Status ${err.status}: ${err.message}`)
|
||||
assert.equal(err.message, message)
|
||||
return true
|
||||
})
|
||||
const ANZAHL = 'Eine Rechnung braucht 1 bis 20 Positionen.'
|
||||
const ok = { description: 'Domain', quantity: 1, unitAmountCents: 1200 }
|
||||
|
||||
test('stundenText: Minuten als Stunden mit Komma, ohne überflüssige Nullen', () => {
|
||||
const faelle = [[0, '0'], [15, '0,25'], [30, '0,5'], [45, '0,75'], [60, '1'], [90, '1,5'], [135, '2,25'], [390, '6,5'], [600, '10']]
|
||||
for (const [minuten, text] of faelle) assert.equal(stundenText(minuten), text, `${minuten} Minuten`)
|
||||
})
|
||||
|
||||
test('leistungszeitraumText: ein Tag, ein Jahr, Jahreswechsel (Halbgeviertstrich U+2013 ohne Leerzeichen)', () => {
|
||||
assert.equal(leistungszeitraumText('2026-10-06', '2026-10-06'), '06.10.2026')
|
||||
assert.equal(leistungszeitraumText('2026-10-01', '2026-10-20'), '01.10.–20.10.2026')
|
||||
assert.equal(leistungszeitraumText('2026-09-28', '2026-10-05'), '28.09.–05.10.2026')
|
||||
assert.equal(leistungszeitraumText('2026-12-28', '2027-01-05'), '28.12.2026–05.01.2027')
|
||||
})
|
||||
|
||||
test('kalenderwoche: ISO-Woche samt ISO-Jahr, gueltigerTag: nur Tage, die es gibt', () => {
|
||||
const faelle = [
|
||||
['2026-10-05', 2026, 41], ['2026-10-06', 2026, 41], ['2026-10-11', 2026, 41], ['2026-10-12', 2026, 42],
|
||||
['2025-12-29', 2026, 1], ['2026-01-01', 2026, 1], ['2026-12-31', 2026, 53], ['2027-01-03', 2026, 53],
|
||||
['2027-01-04', 2027, 1], ['2024-12-30', 2025, 1], ['2021-01-03', 2020, 53],
|
||||
]
|
||||
for (const [tag, jahr, woche] of faelle) assert.deepEqual(kalenderwoche(tag), { jahr, woche }, tag)
|
||||
assert.throws(() => kalenderwoche('2026-02-30'), RangeError)
|
||||
assert.equal(gueltigerTag('2026-02-28'), true)
|
||||
for (const tag of ['2026-02-30', '2026-13-01', '06.10.2026', '2026-1-1', '', undefined, 20261006]) assert.equal(gueltigerTag(tag), false, String(tag))
|
||||
})
|
||||
|
||||
test('kuerzen: höchstens max Zeichen, Auslassungszeichen am Ende, kein halbes Emoji', () => {
|
||||
assert.equal(kuerzen('kurz', 10), 'kurz')
|
||||
assert.equal(kuerzen('abcdefghij', 10), 'abcdefghij')
|
||||
assert.equal(kuerzen('abcdefghijk', 10), 'abcdefghi…')
|
||||
assert.equal(kuerzen('ab cdefghijk', 4), 'ab…')
|
||||
assert.equal(kuerzen('abcd😀fghij', 6), 'abcd…')
|
||||
})
|
||||
|
||||
test('festePositionen: Eingabe wie bisher, Beschreibung getrimmt, Schlüssel pos-n, Betrag = Menge × Einzelpreis', () => {
|
||||
assert.deepEqual(festePositionen([{ description: ' Website ', quantity: 2, unitAmountCents: 5000 }, ok]), [
|
||||
{ schluessel: 'pos-1', beschreibung: 'Website', menge: 2, einzelpreisCents: 5000, betragCents: 10000 },
|
||||
{ schluessel: 'pos-2', beschreibung: 'Domain', menge: 1, einzelpreisCents: 1200, betragCents: 1200 },
|
||||
])
|
||||
assert.equal(festePositionen(Array.from({ length: 20 }, () => ok)).length, 20)
|
||||
})
|
||||
|
||||
test('festePositionen: Grenzen und Meldungen wie bei der bisherigen Rechnung', () => {
|
||||
wirft(() => festePositionen([]), 400, ANZAHL)
|
||||
wirft(() => festePositionen(undefined), 400, ANZAHL)
|
||||
wirft(() => festePositionen(Array.from({ length: 21 }, () => ok)), 400, ANZAHL)
|
||||
for (const description of ['', ' ', 'x'.repeat(201), 5]) wirft(() => festePositionen([{ ...ok, description }]), 400, 'Position 1: Die Beschreibung braucht 1 bis 200 Zeichen.')
|
||||
for (const quantity of [0, 1000, 1.5, '2', -1, null]) wirft(() => festePositionen([ok, { ...ok, quantity }]), 400, 'Position 2: Die Menge muss eine ganze Zahl von 1 bis 999 sein.')
|
||||
for (const unitAmountCents of [0, 10_000_001, 12.5, '5000', -100, null]) {
|
||||
wirft(() => festePositionen([{ ...ok, unitAmountCents }]), 400, 'Position 1: Der Einzelpreis muss zwischen 0,01 € und 100.000,00 € liegen.')
|
||||
}
|
||||
const neun = Array.from({ length: 9 }, (_, i) => ({ description: `Teil ${i + 1}`, quantity: 1, unitAmountCents: 10_000_000 }))
|
||||
wirft(() => festePositionen([...neun, { description: 'Rest', quantity: 1, unitAmountCents: 10_000_000 }]), 400, 'Rechnungssumme zu hoch.')
|
||||
assert.equal(festePositionen([...neun, { description: 'Rest', quantity: 1, unitAmountCents: 9_999_999 }]).at(-1).betragCents, 9_999_999)
|
||||
})
|
||||
|
||||
test('zusatzPositionen: leer erlaubt, Schlüssel zusatz-n, gleiche Prüfungen; pruefePositionen: Zahl (mit wählbarer Obergrenze), Texte, Summe mit Minuszeilen', () => {
|
||||
for (const leer of [undefined, null, []]) assert.deepEqual(zusatzPositionen(leer), [])
|
||||
assert.deepEqual(zusatzPositionen([{ description: 'Domain', quantity: 2, unitAmountCents: 1500 }]), [
|
||||
{ schluessel: 'zusatz-1', beschreibung: 'Domain', menge: 2, einzelpreisCents: 1500, betragCents: 3000 },
|
||||
])
|
||||
wirft(() => zusatzPositionen('Domain'), 400, ANZAHL)
|
||||
wirft(() => zusatzPositionen(Array.from({ length: 21 }, () => ok)), 400, ANZAHL)
|
||||
wirft(() => zusatzPositionen([{ ...ok, description: '' }]), 400, 'Position 1: Die Beschreibung braucht 1 bis 200 Zeichen.')
|
||||
|
||||
const pos = (beschreibung, cents) => ({ schluessel: 'x', beschreibung, menge: 1, einzelpreisCents: cents, betragCents: cents })
|
||||
assert.equal(pruefePositionen([pos('A', 5000), pos('abzüglich Anzahlung', -2000)]), 3000)
|
||||
assert.equal(pruefePositionen([pos('A', 99_999_999)]), 99_999_999)
|
||||
wirft(() => pruefePositionen([]), 400, ANZAHL)
|
||||
wirft(() => pruefePositionen(Array.from({ length: 21 }, () => pos('A', 1))), 400, ANZAHL)
|
||||
// eine Schlussrechnung darf bis 40 Zeilen haben: der Aufrufer hebt die Obergrenze an
|
||||
assert.equal(pruefePositionen(Array.from({ length: 40 }, () => pos('A', 1)), { max: 40 }), 40)
|
||||
wirft(() => pruefePositionen(Array.from({ length: 41 }, () => pos('A', 1)), { max: 40 }), 400, 'Eine Rechnung braucht 1 bis 40 Positionen.')
|
||||
wirft(() => pruefePositionen([], { max: 40 }), 400, 'Eine Rechnung braucht 1 bis 40 Positionen.')
|
||||
wirft(() => pruefePositionen([pos('A', 1), pos('', 1)]), 400, 'Position 2: Die Beschreibung braucht 1 bis 200 Zeichen.')
|
||||
wirft(() => pruefePositionen([pos('x'.repeat(201), 1)]), 400, 'Position 1: Die Beschreibung braucht 1 bis 200 Zeichen.')
|
||||
wirft(() => pruefePositionen([pos('A', 99_999_999), pos('B', 1)]), 400, 'Rechnungssumme zu hoch.')
|
||||
})
|
||||
Reference in new issue
Block a user