Files
Webklar-Kundenbereich/test/auftraegeStripe.test.js
T

166 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test, after, beforeEach, mock } from 'node:test'
import assert from 'node:assert/strict'
import { startAppwriteMock } from './support/appwriteMock.js'
import { startFakeStripe } from './support/fakeStripe.js'
import { schemaKeys } from './support/schema.js'
import { useTestEnv } from './support/testEnv.js'
import { AUFTRAEGE_DATETIMES } from '../server/services/auftraege/schema.js'
const aw = await startAppwriteMock({
schema: { auftraege: schemaKeys('auftraege'), projektRechnungen: schemaKeys('projektRechnungen') },
datetimes: AUFTRAEGE_DATETIMES,
defaultLimit: 25,
})
const st = await startFakeStripe()
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
const { config } = await import('../server/config.js')
const { seedRechnungsDaten, legeAuftragAn, leereRechnungsDaten } = await import('./auftraegeRechnungenHilfe.js')
const {
mapInvoice, createProjectInvoice, rechnungsVorschau, sendInvoiceNow, voidInvoice, deleteDraft, invalidateFinanceCache, resetInvoiceCreateGuardForTests,
} = await import('../server/services/finance/stripeFinance.js')
const { getProjektRechnung } = await import('../server/services/auftraege/projektRechnungenStore.js')
const { getAuftrag } = await import('../server/services/auftraege/auftragStore.js')
const { sperreAuftrag } = await import('../server/services/auftraege/konstanten.js')
const { withLocks } = await import('../server/services/keyedLock.js')
const { berlinDate, formatDe } = await import('../server/services/finance/dates.js')
after(() => Promise.all([aw.close(), st.close()]))
seedRechnungsDaten(aw)
config.auftraege.mode = 'on'
beforeEach(async () => {
mock.restoreAll()
await leereRechnungsDaten(aw, st)
invalidateFinanceCache()
resetInvoiceCreateGuardForTests()
})
const NBSP = String.fromCharCode(160) // geschütztes Leerzeichen U+00A0, wie euroDe es vor € setzt
const POSTEN = [{ description: 'Domain', quantity: 1, unitAmountCents: 1200 }]
const callsTo = (method, path) => st.calls.filter((c) => c.method === method && (path instanceof RegExp ? path.test(c.path) : c.path === path))
const writes = () => st.calls.filter((c) => c.method !== 'GET')
const wirft = (promise, status, message) =>
assert.rejects(promise, (err) => {
assert.equal(err.status, status, `Status ${err.status}: ${err.message}`)
assert.equal(err.message, message)
return true
})
test('mapInvoice: Projektfelder aus den Metadaten, art mit Rückfall abo oder frei', () => {
const roh = (over = {}) => ({ id: 'in_1', object: 'invoice', status: 'open', customer: 'cus_1', created: 1_790_000_000, total: 1000, metadata: {}, lines: { object: 'list', data: [] }, ...over })
const felder = (r) => [r.projektId, r.projektName, r.art, r.auftragId, r.auftragNummer]
const heute = { today: '2026-10-06' }
assert.deepEqual(felder(mapInvoice(roh(), heute)), ['', '', 'frei', '', ''])
assert.deepEqual(felder(mapInvoice(roh({ subscription: 'sub_1' }), heute)), ['', '', 'abo', '', ''])
const metadata = { projektId: 'p1', projektName: 'Website Wolf', art: 'schluss', auftragId: 'a1', auftragNummer: 'AB-2026-0002' }
assert.deepEqual(felder(mapInvoice(roh({ metadata }), heute)), ['p1', 'Website Wolf', 'schluss', 'a1', 'AB-2026-0002'])
for (const art of ['quatsch', 'abo', '']) assert.equal(mapInvoice(roh({ metadata: { art } }), heute).art, 'frei', `art "${art}"`)
})
test('createProjectInvoice frei: A4 für jede Rechnung, Art in den Metadaten, keine Zusatzfelder, kein Projektdatensatz', async () => {
const rechnung = await createProjectInvoice({ customerId: 'k1', items: POSTEN, createdBy: 'kenso@webklar.com' })
const [create] = callsTo('POST', '/v1/invoices')
assert.equal(create.body['rendering[pdf][page_size]'], 'a4')
assert.equal(create.body['metadata[art]'], 'frei')
assert.equal(create.body['metadata[webklarCustomerId]'], 'k1')
assert.ok(!Object.keys(create.body).some((k) => k.startsWith('custom_fields')))
assert.ok(!Object.keys(create.body).some((k) => /^metadata\[(projektId|projektName|auftragId|auftragNummer)\]$/.test(k)))
assert.deepEqual([rechnung.art, rechnung.status, rechnung.totalCents, rechnung.projektId], ['frei', 'entwurf', 1200, ''])
assert.equal(aw.docs(config.collections.projektRechnungen).size, 0)
})
test('createProjectInvoice festbetrag: Zusatzfeld Projekt, Metadaten und ein Projektdatensatz mit der Rechnungs-ID', async () => {
const items = [{ description: 'Erstellung der Website anelia-wolf.com, Pauschalpreis laut Absprache', quantity: 1, unitAmountCents: 50000 }]
const rechnung = await createProjectInvoice({ customerId: 'k1', art: 'festbetrag', projektId: 'p1', items, createdBy: 'kenso@webklar.com' })
const [create] = callsTo('POST', '/v1/invoices')
assert.equal(create.body['custom_fields[0][name]'], 'Projekt')
assert.equal(create.body['custom_fields[0][value]'], 'Website anelia-wolf.com')
assert.equal(create.body['custom_fields[1][name]'], undefined)
assert.equal(create.body['rendering[pdf][page_size]'], 'a4')
assert.deepEqual([create.body['metadata[art]'], create.body['metadata[projektId]'], create.body['metadata[projektName]']], ['festbetrag', 'p1', 'Website anelia-wolf.com'])
assert.deepEqual([rechnung.art, rechnung.projektId, rechnung.projektName, rechnung.totalCents], ['festbetrag', 'p1', 'Website anelia-wolf.com', 50000])
const doc = await getProjektRechnung(rechnung.id)
assert.deepEqual(
[doc.invoiceId, doc.kundenId, doc.projektId, doc.auftragId, doc.art, doc.betragCents, doc.rechnungsnummer, doc.aktiv, doc.erstelltVon],
[rechnung.id, 'k1', 'p1', '', 'festbetrag', 50000, '', true, 'kenso@webklar.com']
)
})
test('createProjectInvoice aufwand: eine Zeile je Tag bei Stripe, Blätter im Projektdatensatz, Zeitraum als Zusatzfeld', async () => {
const rechnung = await createProjectInvoice({ customerId: 'k1', art: 'aufwand', projektId: 'p1', arbeitsblattIds: ['ws1', 'ws2', 'ws3'], createdBy: 'k' })
const items = callsTo('POST', '/v1/invoiceitems')
assert.deepEqual(items.map((c) => [c.body.quantity, c.body.unit_amount_decimal, c.body.amount]), [['1', '22500', undefined], ['1', '10000', undefined]])
assert.ok(items[0].body.description.startsWith('06.10.2026: '))
const [create] = callsTo('POST', '/v1/invoices')
assert.deepEqual([0, 1].map((i) => create.body[`custom_fields[${i}][name]`]), ['Projekt', 'Leistungszeitraum'])
assert.equal(create.body['custom_fields[1][value]'], '06.10.–07.10.2026')
assert.equal(rechnung.totalCents, 32500)
assert.deepEqual((await getProjektRechnung(rechnung.id)).arbeitsblattIds, ['ws1', 'ws2', 'ws3'])
})
test('createProjectInvoice schluss: positive Zeilen mit Menge und Einzelpreis, die Minuszeile nur mit amount (ohne quantity)', async () => {
const ab = await legeAuftragAn()
const anzahlung = await createProjectInvoice({ customerId: 'k1', art: 'anzahlung', auftragId: ab.id, senden: true, createdBy: 'k' })
assert.deepEqual([anzahlung.totalCents, anzahlung.art, anzahlung.auftragNummer, anzahlung.status], [40000, 'anzahlung', 'AB-2026-0002', 'offen'])
st.calls.length = 0
const schluss = await createProjectInvoice({ customerId: 'k1', art: 'schluss', auftragId: ab.id, createdBy: 'k' })
const [create] = callsTo('POST', '/v1/invoices')
assert.deepEqual(
[0, 1].map((i) => [create.body[`custom_fields[${i}][name]`], create.body[`custom_fields[${i}][value]`]]),
[['Projekt', 'Website anelia-wolf.com'], ['Auftrag', 'AB-2026-0002 vom 06.10.2026']]
)
const items = callsTo('POST', '/v1/invoiceitems')
const kunde = items[0].body.customer
assert.deepEqual(items.map((c) => c.body), [
{ customer: kunde, invoice: schluss.id, description: 'Website', quantity: '1', unit_amount_decimal: '60000', currency: 'eur' },
{ customer: kunde, invoice: schluss.id, description: 'Texte', quantity: '2', unit_amount_decimal: '10000', currency: 'eur' },
{ customer: kunde, invoice: schluss.id, description: `abzüglich Anzahlung, Rechnung ${anzahlung.number} vom ${formatDe(berlinDate())}`, amount: '-40000', currency: 'eur' },
])
assert.deepEqual([schluss.totalCents, schluss.art, schluss.status], [40000, 'schluss', 'entwurf'])
})
test('createProjectInvoice: scheitert der Projektdatensatz, wird der Entwurf bei Stripe gelöscht und nichts bleibt zurück', async () => {
const eingabe = { customerId: 'k1', art: 'festbetrag', projektId: 'p1', items: POSTEN, createdBy: 'k' }
aw.failNext({ collection: config.collections.projektRechnungen, method: 'POST', status: 500 })
await assert.rejects(createProjectInvoice(eingabe))
assert.equal(st.invoices.size, 0)
assert.equal(callsTo('DELETE', /^\/v1\/invoices\/in_/).length, 1)
assert.equal(aw.docs(config.collections.projektRechnungen).size, 0)
// der Schutz vor doppeltem Anlegen ist wieder frei
assert.equal((await createProjectInvoice(eingabe)).status, 'entwurf')
})
test('createProjectInvoice: der Fingerabdruck unterscheidet Art, Projekt, Auftrag und Anzahlung, gleicher Inhalt bleibt 30 Sekunden gesperrt', async () => {
const basis = { customerId: 'k1', items: POSTEN }
const GESPERRT = 'Diese Rechnung wird gerade schon erstellt.'
await createProjectInvoice(basis)
await createProjectInvoice({ ...basis, art: 'festbetrag', projektId: 'p1' })
await createProjectInvoice({ ...basis, art: 'festbetrag', projektId: 'p3' })
await wirft(createProjectInvoice({ ...basis, art: 'festbetrag', projektId: 'p3' }), 409, GESPERRT)
const ab = await legeAuftragAn()
await createProjectInvoice({ customerId: 'k1', art: 'anzahlung', auftragId: ab.id, anzahlung: { prozent: 50 } })
await createProjectInvoice({ customerId: 'k1', art: 'anzahlung', auftragId: ab.id, anzahlung: { prozent: 25 } })
await wirft(createProjectInvoice({ customerId: 'k1', art: 'anzahlung', auftragId: ab.id, anzahlung: { prozent: 25 } }), 409, GESPERRT)
assert.equal(st.invoices.size, 5)
})
test('rechnungsVorschau: Positionen, Summe, Zeitraum und Zusatzfelder ohne Stripe-Schreibzugriff und ohne Projektdatensatz', async () => {
const v = await rechnungsVorschau({ customerId: 'k1', art: 'aufwand', projektId: 'p1' })
assert.deepEqual(Object.keys(v).sort(), ['customFields', 'leistungszeitraum', 'positionen', 'summeCents'])
assert.equal(v.summeCents, 32500)
assert.deepEqual(v.positionen.map((p) => p.schluessel), ['2026-10-06', '2026-10-07'])
assert.equal(v.leistungszeitraum, '06.10.–07.10.2026')
assert.deepEqual(v.customFields.map((f) => f.name), ['Projekt', 'Leistungszeitraum'])
assert.equal(writes().length, 0)
assert.equal(aw.docs(config.collections.projektRechnungen).size, 0)
})
test('rechnungsVorschau: dieselben Prüfungen wie das Anlegen; Anzahlung ohne Angaben zeigt den Standardwert', async () => {
await createProjectInvoice({ customerId: 'k1', art: 'aufwand', projektId: 'p1', arbeitsblattIds: ['ws1'] })
await wirft(rechnungsVorschau({ customerId: 'k1', art: 'aufwand', projektId: 'p1', arbeitsblattIds: ['ws1', 'ws3'] }), 409, 'Arbeitsblatt bereits abgerechnet (Entwurf).')
await wirft(rechnungsVorschau({ customerId: 'k1', art: 'aufwand', projektId: 'p2' }), 404, 'Projekt nicht gefunden.')
const ab = await legeAuftragAn()
const v = await rechnungsVorschau({ customerId: 'k1', art: 'anzahlung', auftragId: ab.id })
assert.deepEqual([v.summeCents, v.positionen[0].schluessel], [40000, 'anzahlung'])
assert.equal(aw.docs(config.collections.projektRechnungen).size, 1, 'nur der Entwurf von oben')
})