230 lines
13 KiB
JavaScript
230 lines
13 KiB
JavaScript
import { test, after, beforeEach, mock } from 'node:test'
|
||
import assert from 'node:assert/strict'
|
||
import fs from 'node:fs'
|
||
import path from 'node:path'
|
||
import {
|
||
starteUmgebung, KUNDE_ID, VON, MELDUNG, festpreisEingabe, kundeDoc, baueDeps, sha256, abgelehnt,
|
||
ergebnis, tor, warte, wartebis, dateienIn,
|
||
} from './helpers/auftraegeAb.js'
|
||
|
||
const umg = await starteUmgebung()
|
||
const { aw, config, uploadDir } = umg
|
||
after(() => umg.schliesse())
|
||
beforeEach(umg.zuruecksetzen)
|
||
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
|
||
const { getAuftrag, listAuftraege, updateAuftrag } = await import('../server/services/auftraege/auftragStore.js')
|
||
const { lesePdf } = await import('../server/services/auftraege/ablage.js')
|
||
const { kundeSnapshot } = await import('../server/services/auftraege/auftragSicht.js')
|
||
const { TEXTE_FASSUNG } = await import('../server/services/auftraege/texte.js')
|
||
const { AGB_FASSUNG, TOKEN_GUELTIG_TAGE } = await import('../server/services/auftraege/konstanten.js')
|
||
const { AuftragError } = await import('../server/services/auftraege/fehler.js')
|
||
const { legeEntwurfAn, aendereEntwurf, loescheEntwurf, sendeAuftrag } = await import('../server/services/auftraege/auftraege.js')
|
||
|
||
const JETZT = new Date('2026-10-06T10:00:00Z') // Berlin: 12:00
|
||
const neuerEntwurf = (patch) => legeEntwurfAn(festpreisEingabe(patch), { von: VON })
|
||
const sende = (id, deps, extra = {}) => sendeAuftrag(id, { von: VON, deps, jetzt: JETZT, ...extra })
|
||
|
||
const VOR_DER_NUMMER = [
|
||
['Interessent', { customerStatus: 'lead' }, 400, MELDUNG.keinFesterKunde],
|
||
['verlorener Kunde (Schreibweise egal)', { customerStatus: ' Lost ' }, 400, MELDUNG.keinFesterKunde],
|
||
['Kundenart fehlt', { kundenart: '' }, 400, MELDUNG.keineKundenart],
|
||
['Kundenart unbekannt', { kundenart: 'firma' }, 400, MELDUNG.keineKundenart],
|
||
['E-Mail fehlt', { email: '' }, 400, MELDUNG.keineMail],
|
||
['E-Mail nur Leerzeichen', { email: ' ' }, 400, MELDUNG.keineMail],
|
||
['Straße fehlt', { street: '' }, 409, MELDUNG.adresse],
|
||
['PLZ nur Leerzeichen', { postalCode: ' ' }, 409, MELDUNG.adresse],
|
||
['Ort fehlt', { city: '' }, 409, MELDUNG.adresse],
|
||
['Interessent hat Vorrang vor allem anderen', { customerStatus: 'lead', kundenart: '', email: '', street: '' }, 400, MELDUNG.keinFesterKunde],
|
||
['Kundenart kommt vor E-Mail und Anschrift', { kundenart: '', email: '', street: '' }, 400, MELDUNG.keineKundenart],
|
||
['E-Mail kommt vor der Anschrift', { email: '', street: '' }, 400, MELDUNG.keineMail],
|
||
]
|
||
|
||
test('Senden: unvollständige Kundendaten werden vor der Nummer abgelehnt – nichts wird erzeugt, gespeichert oder verbraucht', async () => {
|
||
const entwurf = await neuerEntwurf()
|
||
const { deps, calls } = baueDeps()
|
||
for (const [name, patch, status, meldung] of VOR_DER_NUMMER) {
|
||
aw.seed(config.collections.customers, kundeDoc(patch))
|
||
await abgelehnt(sende(entwurf.id, deps), status, meldung, name)
|
||
}
|
||
assert.deepEqual([calls.pdf.length, calls.token.length, calls.mail.length], [0, 0, 0])
|
||
assert.deepEqual(dateienIn(uploadDir), [])
|
||
const unveraendert = await getAuftrag(entwurf.id)
|
||
assert.deepEqual([unveraendert.status, unveraendert.nummer, unveraendert.jahr], ['entwurf', '', null])
|
||
aw.seed(config.collections.customers, kundeDoc()) // der Kunde ist wieder vollständig: die erste echte Nummer ist 0001
|
||
assert.equal((await sende(entwurf.id, deps)).auftrag.nummer, 'AB-2026-0001')
|
||
})
|
||
|
||
test('Senden: der Entwurf wird vor der Nummer noch einmal geprüft (beschädigte Daten ergeben die Feldmeldung)', async () => {
|
||
const entwurf = await neuerEntwurf()
|
||
await updateAuftrag(entwurf.id, { positionen: [] })
|
||
await abgelehnt(sende(entwurf.id, baueDeps().deps), 400, MELDUNG.positionen)
|
||
})
|
||
|
||
test('Senden: Nummer, eingefrorener Kunde, PDF mit Prüfsumme, Token und Mail', async () => {
|
||
aw.seed(config.collections.financeSettings, { $id: 'main', steuernummer: '12/345/67890', bankDetailsText: 'IBAN DE00 0000 0000 0000 0000 00' })
|
||
const entwurf = await neuerEntwurf()
|
||
const { deps, calls } = baueDeps()
|
||
const { auftrag, mailFehler } = await sende(entwurf.id, deps)
|
||
assert.equal(mailFehler, false)
|
||
assert.deepEqual([auftrag.id, auftrag.status, auftrag.nummer, auftrag.jahr, auftrag.laufnummer], [entwurf.id, 'versendet', 'AB-2026-0001', 2026, 1])
|
||
assert.equal(new Date(auftrag.gesendetAm).getTime(), JETZT.getTime())
|
||
assert.equal(auftrag.gesendetAn, 'anna@example.invalid')
|
||
assert.deepEqual(auftrag.kunde, kundeSnapshot(kundeDoc()))
|
||
assert.equal(auftrag.kunde.kundenart, 'unternehmen')
|
||
assert.deepEqual([auftrag.texteFassung, auftrag.agbFassung], [TEXTE_FASSUNG, AGB_FASSUNG])
|
||
assert.deepEqual([auftrag.mailStatus, auftrag.mailFehler], ['gesendet', ''])
|
||
// PDF: einmal mit der Nummer und dem Kunden-Schnappschuss erzeugt, abgelegt, Prüfsumme gespeichert
|
||
const pdf = Buffer.from('%PDF-1.4 AB-2026-0001')
|
||
assert.equal(calls.pdf.length, 1)
|
||
assert.equal(calls.pdf[0].auftrag.nummer, 'AB-2026-0001')
|
||
assert.deepEqual(calls.pdf[0].auftrag.kunde, kundeSnapshot(kundeDoc()))
|
||
assert.deepEqual(calls.pdf[0].optionen, { settings: { steuernummer: '12/345/67890', bankDetailsText: 'IBAN DE00 0000 0000 0000 0000 00' } })
|
||
assert.equal(auftrag.pdfDatei, `${KUNDE_ID}/${entwurf.id}.pdf`)
|
||
assert.equal(auftrag.pdfSha256, sha256(pdf))
|
||
assert.deepEqual(await lesePdf(auftrag.pdfDatei), pdf)
|
||
// Token: im Dokument nur der Hash, 60 Tage gültig ab dem Versand
|
||
assert.equal(calls.token.length, 1)
|
||
assert.equal(auftrag.tokenHash, sha256(calls.token[0]))
|
||
assert.equal(new Date(auftrag.tokenGueltigBis).getTime(), JETZT.getTime() + TOKEN_GUELTIG_TAGE * 86_400_000)
|
||
// Mail: einmal, mit dem gespeicherten Dokument, dem PDF und dem Klartext-Token
|
||
assert.equal(calls.mail.length, 1)
|
||
assert.equal(calls.mail[0].auftrag.nummer, 'AB-2026-0001')
|
||
assert.deepEqual(calls.mail[0].pdf, pdf)
|
||
assert.deepEqual([calls.mail[0].token, calls.mail[0].erinnerung], [calls.token[0], false])
|
||
assert.deepEqual(await getAuftrag(entwurf.id), auftrag)
|
||
})
|
||
|
||
test('Senden: Nummern zählen fortlaufend, im neuen Berliner Jahr beginnt wieder 0001', async () => {
|
||
const { deps } = baueDeps()
|
||
const nummern = []
|
||
for (let i = 0; i < 2; i += 1) nummern.push((await sende((await neuerEntwurf()).id, deps)).auftrag.nummer)
|
||
const neujahr = await sende((await neuerEntwurf()).id, deps, { jetzt: new Date('2026-12-31T23:30:00Z') })
|
||
assert.deepEqual(nummern, ['AB-2026-0001', 'AB-2026-0002'])
|
||
assert.deepEqual([neujahr.auftrag.nummer, neujahr.auftrag.jahr, neujahr.auftrag.laufnummer], ['AB-2027-0001', 2027, 1])
|
||
assert.equal(new Date(neujahr.auftrag.gesendetAm).toISOString(), '2026-12-31T23:30:00.000Z')
|
||
})
|
||
|
||
test('Senden: nur Entwürfe (409), unbekannte Aufträge (404) und Kunden (404)', async () => {
|
||
const { deps } = baueDeps()
|
||
const entwurf = await neuerEntwurf()
|
||
await sende(entwurf.id, deps)
|
||
await abgelehnt(sende(entwurf.id, deps), 409, MELDUNG.nurEntwurfSenden)
|
||
await abgelehnt(sende('a_gibtsnicht', deps), 404, MELDUNG.auftragFehlt)
|
||
const zweiter = await neuerEntwurf()
|
||
await deleteDocument(config.collections.customers, KUNDE_ID)
|
||
await abgelehnt(sende(zweiter.id, deps), 404, MELDUNG.kundeFehlt)
|
||
})
|
||
|
||
test('Senden: scheitert die PDF-Erzeugung (Fehler, leer oder unvollständiger Entwurf), bleibt alles wie vorher und die Nummer ist nicht verbraucht', async () => {
|
||
mock.method(console, 'error', () => {})
|
||
const entwurf = await neuerEntwurf()
|
||
const faelle = [
|
||
[baueDeps({ pdfFehler: new Error('pdfkit kaputt') }), 500, MELDUNG.pdf],
|
||
[baueDeps({ pdf: Buffer.alloc(0) }), 500, MELDUNG.pdf],
|
||
[baueDeps({ pdfFehler: new AuftragError(409, 'Entwurf unvollständig: Anschrift fehlt.') }), 409, 'Entwurf unvollständig: Anschrift fehlt.'],
|
||
]
|
||
for (const [fehlerhaft, status, meldung] of faelle) {
|
||
await abgelehnt(sende(entwurf.id, fehlerhaft.deps), status, meldung)
|
||
assert.deepEqual([fehlerhaft.calls.token.length, fehlerhaft.calls.mail.length], [0, 0])
|
||
}
|
||
assert.deepEqual(dateienIn(uploadDir), [])
|
||
const danach = await getAuftrag(entwurf.id)
|
||
assert.deepEqual([danach.status, danach.nummer, danach.jahr], ['entwurf', '', null])
|
||
assert.ok(!danach.pdfDatei && !danach.tokenHash)
|
||
assert.equal((await sende(entwurf.id, baueDeps().deps)).auftrag.nummer, 'AB-2026-0001')
|
||
})
|
||
|
||
test('Senden: eine schon vorhandene Zieldatei wird nie überschrieben – Fehler, Entwurf und Datei bleiben', async () => {
|
||
mock.method(console, 'error', () => {})
|
||
const entwurf = await neuerEntwurf()
|
||
const ziel = path.join(uploadDir, KUNDE_ID, `${entwurf.id}.pdf`)
|
||
fs.mkdirSync(path.dirname(ziel), { recursive: true })
|
||
fs.writeFileSync(ziel, 'Rest eines Absturzes')
|
||
const { deps, calls } = baueDeps()
|
||
await abgelehnt(sende(entwurf.id, deps), 500, MELDUNG.pdf)
|
||
assert.equal(fs.readFileSync(ziel, 'utf8'), 'Rest eines Absturzes')
|
||
assert.equal(calls.mail.length, 0)
|
||
assert.equal((await getAuftrag(entwurf.id)).status, 'entwurf')
|
||
})
|
||
|
||
test('Senden: scheitert das Speichern der Auftragsbestätigung, wird die eben abgelegte Datei entfernt und das Senden lässt sich wiederholen', async () => {
|
||
mock.method(console, 'error', () => {})
|
||
const entwurf = await neuerEntwurf()
|
||
const { deps, calls } = baueDeps()
|
||
aw.failNext({ collection: config.collections.auftraege, method: 'PATCH', status: 500 })
|
||
await assert.rejects(sende(entwurf.id, deps), (err) => err.status === 500)
|
||
assert.equal(calls.mail.length, 0)
|
||
assert.deepEqual(dateienIn(uploadDir), [])
|
||
assert.equal((await getAuftrag(entwurf.id)).status, 'entwurf')
|
||
const { auftrag } = await sende(entwurf.id, deps)
|
||
assert.equal(auftrag.nummer, 'AB-2026-0001')
|
||
assert.deepEqual(dateienIn(uploadDir), [`${KUNDE_ID}/${entwurf.id}.pdf`])
|
||
})
|
||
|
||
test('Senden: scheitert nur die Mail, bleibt die AB versendet (mailStatus fehler) und der Fehlertext verrät nichts Internes', async () => {
|
||
mock.method(console, 'error', () => {})
|
||
const [e1, e2] = [await neuerEntwurf(), await neuerEntwurf()]
|
||
const mitCode = baueDeps({ mailFehler: Object.assign(new Error('535 Login smtp.example.invalid'), { code: 'EAUTH' }) })
|
||
const r1 = await sende(e1.id, mitCode.deps)
|
||
assert.equal(r1.mailFehler, true)
|
||
assert.deepEqual(
|
||
[r1.auftrag.status, r1.auftrag.nummer, r1.auftrag.mailStatus, r1.auftrag.mailFehler],
|
||
['versendet', 'AB-2026-0001', 'fehler', 'E-Mail konnte nicht gesendet werden (EAUTH).'],
|
||
)
|
||
assert.ok(r1.auftrag.pdfDatei && r1.auftrag.tokenHash)
|
||
assert.deepEqual(await getAuftrag(e1.id), r1.auftrag)
|
||
const ohneCode = baueDeps({ mailFehler: new Error('Verbindung zu mail.example.invalid abgebrochen') })
|
||
const r2 = await sende(e2.id, ohneCode.deps)
|
||
assert.deepEqual([r2.auftrag.nummer, r2.auftrag.mailStatus, r2.auftrag.mailFehler], ['AB-2026-0002', 'fehler', 'E-Mail konnte nicht gesendet werden.'])
|
||
})
|
||
|
||
test('Senden: zwei Entwürfe gleichzeitig bekommen verschiedene Nummern', async () => {
|
||
const [a, b] = [await neuerEntwurf(), await neuerEntwurf()]
|
||
const { deps } = baueDeps({ pdfWarten: warte(30) })
|
||
const ergebnisse = await Promise.all([sende(a.id, deps), sende(b.id, deps)])
|
||
assert.deepEqual(ergebnisse.map((e) => e.auftrag.nummer).sort(), ['AB-2026-0001', 'AB-2026-0002'])
|
||
assert.equal(new Set(ergebnisse.map((e) => e.auftrag.pdfDatei)).size, 2)
|
||
})
|
||
|
||
test('Senden: dieselbe AB zweimal gleichzeitig (Doppelklick) – einmal gesendet, das zweite Mal 409, genau eine Nummer und eine Mail', async () => {
|
||
const entwurf = await neuerEntwurf()
|
||
const { deps, calls } = baueDeps({ pdfWarten: warte(30) })
|
||
const ergebnisse = await Promise.allSettled([sende(entwurf.id, deps), sende(entwurf.id, deps)])
|
||
assert.deepEqual(ergebnisse.map((r) => r.status).sort(), ['fulfilled', 'rejected'])
|
||
const abgewiesen = ergebnisse.find((r) => r.status === 'rejected').reason
|
||
assert.deepEqual([abgewiesen.status, abgewiesen.message], [409, MELDUNG.nurEntwurfSenden])
|
||
assert.deepEqual([calls.pdf.length, calls.mail.length], [1, 1])
|
||
assert.deepEqual((await listAuftraege({ jahr: 2026 })).map((a) => a.nummer), ['AB-2026-0001'])
|
||
})
|
||
|
||
test('Senden hält die Sperre: Ändern und Löschen währenddessen warten und scheitern danach mit 409', async () => {
|
||
const entwurf = await neuerEntwurf()
|
||
const pdfTor = tor()
|
||
const { deps, calls } = baueDeps({ pdfWarten: pdfTor.promise })
|
||
const senden = ergebnis(sende(entwurf.id, deps))
|
||
await wartebis(() => calls.pdf.length === 1) // das Senden hält die Sperre und wartet auf das PDF
|
||
const aendern = ergebnis(aendereEntwurf(entwurf.id, { titel: 'Heimlich geändert' }))
|
||
const loeschen = ergebnis(loescheEntwurf(entwurf.id))
|
||
await warte(30) // beide sind unterwegs, dürfen aber noch nichts geschrieben haben
|
||
assert.equal((await getAuftrag(entwurf.id)).titel, 'Website Beispiel')
|
||
pdfTor.freigeben()
|
||
const [s, a, l] = await Promise.all([senden, aendern, loeschen])
|
||
assert.equal(s.wert.auftrag.status, 'versendet')
|
||
assert.deepEqual([a.fehler?.status, a.fehler?.message], [409, MELDUNG.nurEntwurfAendern])
|
||
assert.deepEqual([l.fehler?.status, l.fehler?.message], [409, MELDUNG.nurEntwurfLoeschen])
|
||
const danach = await getAuftrag(entwurf.id)
|
||
assert.deepEqual([danach.titel, danach.status], ['Website Beispiel', 'versendet'])
|
||
})
|
||
|
||
test('Senden: während die Mail unterwegs ist, ist die Sperre schon frei (die nächste AB bekommt ihre Nummer)', async () => {
|
||
const [a, b] = [await neuerEntwurf(), await neuerEntwurf()]
|
||
const mailTor = tor()
|
||
const langsam = baueDeps({ mailWarten: mailTor.promise })
|
||
const ersteMail = ergebnis(sende(a.id, langsam.deps))
|
||
await wartebis(() => langsam.calls.mail.length === 1)
|
||
const zweite = await sende(b.id, baueDeps().deps) // läuft durch, obwohl die Mail der ersten noch hängt
|
||
assert.equal(zweite.auftrag.nummer, 'AB-2026-0002')
|
||
mailTor.freigeben()
|
||
assert.equal((await ersteMail).wert.auftrag.nummer, 'AB-2026-0001')
|
||
})
|