AB-Kern: Senden unter Sperre, Fehlerpfade, Dateirest und Mail-Fehler

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-07 14:14:29 +00:00
1 parent ed7567397d
commit c74f7eeea7
2 files changed
+181 -21

No files matched your search

+68 -21
View File
@@ -10,13 +10,13 @@ import { getDocument, listDocuments, Query, updateDocument } from '../appwriteAd
import { getSettings } from '../finance/settings.js'
import { withLocks } from '../keyedLock.js'
import { kundeSnapshot } from './auftragSicht.js'
import { speicherePdf } from './ablage.js'
import { entfernePdf, speicherePdf } from './ablage.js'
import { createAuftrag, deleteAuftrag, getAuftrag, updateAuftrag } from './auftragStore.js'
import { AuftragError } from './fehler.js'
import { vergibNummer } from './nummern.js'
import { TEXTE_FASSUNG } from './texte.js'
import {
AGB_FASSUNG, AUFTRAG_ARTEN, AUFTRAG_WEGE, KUNDENARTEN, LIMITS, sperreAuftrag, STUNDENSATZ_STANDARD_CENTS, VIERTELSTUNDE, ZAHLUNGSPLAN_ARTEN,
AGB_FASSUNG, AUFTRAG_ARTEN, AUFTRAG_WEGE, KUNDENARTEN, LIMITS, SPERRE_NUMMER, sperreAuftrag, STUNDENSATZ_STANDARD_CENTS, VIERTELSTUNDE, ZAHLUNGSPLAN_ARTEN,
} from './konstanten.js'
// ---------------------------------------------------------------------------------------------------------------------
@@ -374,26 +374,73 @@ function pruefeKundeFuerVersand(kundeDoc) {
}
}
/** Entfernt die eben abgelegte PDF, wenn danach etwas scheiterte – aber nur, wenn die AB nachweislich noch ein Entwurf ist. */
async function raeumePdfAuf(auftragId, pdfDatei) {
try {
const aktuell = await getAuftrag(auftragId)
if (aktuell?.status === 'entwurf') await entfernePdf(pdfDatei)
} catch (err) {
logFehler('PDF-Rest entfernen', err)
}
}
/** Fester Text für mailFehler: nie die Meldung von SMTP oder Bibliothek, höchstens ein kurzer Fehlercode. */
function mailFehlerText(err) {
const code = err?.code
return typeof code === 'string' && /^[A-Za-z0-9_.-]{1,40}$/.test(code)
? `E-Mail konnte nicht gesendet werden (${code}).`
: 'E-Mail konnte nicht gesendet werden.'
}
/** Versendet die K31 mit dem gespeicherten Dokument (nach der Sperre) und hält das Ergebnis in mailStatus fest. */
async function versendeMail(d, gespeichert, { pdf, token, erinnerung = false }) {
let mailPatch
try {
await d.sendeAuftragMail(gespeichert, { pdf, token, erinnerung })
mailPatch = { mailStatus: 'gesendet', mailFehler: '' }
} catch (err) {
logFehler('Mailversand', err)
mailPatch = { mailStatus: 'fehler', mailFehler: mailFehlerText(err) }
}
const final = await updateAuftrag(gespeichert.id, mailPatch)
return { auftrag: final, mailFehler: mailPatch.mailStatus === 'fehler' }
}
export async function sendeAuftrag(id, { von, deps, jetzt = new Date() } = {}) {
void von // wird nicht gespeichert (V1 hat kein Feld dafür); die Team-Kopie der Mail belegt den Versand
const d = await ladeDeps(deps, ['erzeugeAuftragPdf', 'neuerToken', 'sendeAuftragMail'])
const entwurf = await ladeAuftrag(id)
if (entwurf.status !== 'entwurf') throw new AuftragError(409, 'Nur ein Entwurf lässt sich senden.')
pruefeEingabe(eingabeAusAuftrag(entwurf))
const kundeDoc = await ladeKunde(entwurf.kundenId)
pruefeKundeFuerVersand(kundeDoc)
const kunde = kundeSnapshot(kundeDoc)
const { steuernummer, bankDetailsText } = await getSettings()
const { nummer, jahr, laufnummer } = await vergibNummer(jetzt)
const gesendetAm = jetzt.toISOString()
const zuSenden = { ...entwurf, status: 'versendet', nummer, jahr, laufnummer, kunde, gesendetAm, texteFassung: TEXTE_FASSUNG, agbFassung: AGB_FASSUNG }
const pdf = await pdfErzeugen(d, zuSenden, { settings: { steuernummer, bankDetailsText } })
const { pdfDatei, pdfSha256 } = await speicherePdf(entwurf.kundenId, entwurf.id, pdf)
const { token, hash, gueltigBis } = d.neuerToken(jetzt)
const gespeichert = await updateAuftrag(entwurf.id, {
status: 'versendet', nummer, jahr, laufnummer, kunde, gesendetAm, texteFassung: TEXTE_FASSUNG, agbFassung: AGB_FASSUNG,
pdfDatei, pdfSha256, gesendetAn: kunde.email, tokenHash: hash, tokenGueltigBis: gueltigBis.toISOString(),
// Nummernvergabe und Statuswechsel unter einer Sperre: zwei Klicks ergeben eine Nummer, Ändern und Löschen warten
const { gespeichert, pdf, token } = await withLocks([...SPERRE_NUMMER, ...sperreAuftrag(id)], async () => {
const entwurf = await ladeAuftrag(id)
if (entwurf.status !== 'entwurf') throw new AuftragError(409, 'Nur ein Entwurf lässt sich senden.')
pruefeEingabe(eingabeAusAuftrag(entwurf))
const kundeDoc = await ladeKunde(entwurf.kundenId)
pruefeKundeFuerVersand(kundeDoc)
const kunde = kundeSnapshot(kundeDoc)
const { steuernummer, bankDetailsText } = await getSettings()
const { nummer, jahr, laufnummer } = await vergibNummer(jetzt)
const gesendetAm = jetzt.toISOString()
const zuSenden = { ...entwurf, status: 'versendet', nummer, jahr, laufnummer, kunde, gesendetAm, texteFassung: TEXTE_FASSUNG, agbFassung: AGB_FASSUNG }
const pdfBytes = await pdfErzeugen(d, zuSenden, { settings: { steuernummer, bankDetailsText } })
let ablage
try {
ablage = await speicherePdf(entwurf.kundenId, entwurf.id, pdfBytes)
} catch (err) {
// eine schon vorhandene Datei gehört nicht diesem Aufruf und bleibt unberührt
logFehler('PDF-Ablage', err)
throw new AuftragError(500, 'Das PDF konnte nicht erzeugt werden.')
}
try {
const neu = d.neuerToken(jetzt)
const gespeichertesDokument = await updateAuftrag(entwurf.id, {
status: 'versendet', nummer, jahr, laufnummer, kunde, gesendetAm, texteFassung: TEXTE_FASSUNG, agbFassung: AGB_FASSUNG,
pdfDatei: ablage.pdfDatei, pdfSha256: ablage.pdfSha256, gesendetAn: kunde.email, tokenHash: neu.hash, tokenGueltigBis: neu.gueltigBis.toISOString(),
})
return { gespeichert: gespeichertesDokument, pdf: pdfBytes, token: neu.token }
} catch (err) {
await raeumePdfAuf(entwurf.id, ablage.pdfDatei)
throw err
}
})
await d.sendeAuftragMail(gespeichert, { pdf, token })
const final = await updateAuftrag(entwurf.id, { mailStatus: 'gesendet', mailFehler: '' })
return { auftrag: final, mailFehler: false }
return versendeMail(d, gespeichert, { pdf, token })
}