ePost-Versand (Letter Submissions)
Zweck
LetterSubmission modelliert den kompletten Versand-Lifecycle eines Briefs über die DocuGuide-ePost-Schnittstelle. Pro Brief kann es mehrere Submissions geben (z. B. wenn ein Versand fehlschlägt und neu probiert wird) — eine davon ist die aktive Submission (Letter.activeSubmissionId).
Was die Submission verfolgt:
- Versand-Status (queued → submitted → in_progress → ready_to_send → sent / failed / cancelled)
- Einschreiben-Variante (
none/einwurf_einschreiben/einschreiben/einschreiben_rueckschein) - Druck-Optionen (Farbe, Duplexdruck)
- Tracking-Events der Deutschen Post (bei Einschreiben)
- Zustellungs-Zeitpunkt und Rückschein-Eingang (bei Einschreiben mit Rückschein)
- Fehler-Details bei DocuGuide-Problemen
Voraussetzungen
Datenmodell
| Feld | Pflicht | Typ | Wirkung |
|---|---|---|---|
letterId | ja | UUID → Letter | Referenz auf den zu versendenden Brief (1:N — pro Brief mehrere Versuche möglich) |
status | auto | ENUM | queued / submitted / in_progress / ready_to_send / sent / failed / cancelled |
epostLetterID | auto | String | Die von DocuGuide nach Submit zurückgegebene ID — Pflicht für Status-Polling |
epostStatusCode | auto | Integer | DocuGuide-Statuscode: 1 Eingangsprüfung / 2 Bearbeitung / 3 Versand-bereit / 4 Versendet / 99 Fehler |
isColor | nein | Boolean | Farbdruck. Default false. Aufpreis bei true. |
isDuplex | nein | Boolean | Duplex-Druck (beidseitig). Default true. |
testFlag | nein | Boolean | Test-Versand (Default aus EPOST_TEST_MODE-env). Bei true wird nicht physisch versendet — nur API-Roundtrip getestet. |
registeredLetter | nein | ENUM | Einschreiben-Variante (siehe Pricing-Tabelle unten) |
attachmentDocumentIds[] | nein | UUID[] → Document | Zusätzliche PDFs, die hinten an den Brief gemerged werden |
pdfHash | auto | String (SHA-256) | Hash über das finale PDF — Replay-Schutz, identisches Re-Submit erkannt |
errorMessage | auto | String | User-freundliche Fehlermeldung |
errorDetail | auto | JSON | Rohe DocuGuide-API-Antwort bei Fehler |
submittedAt | auto | DateTime | Wann an DocuGuide abgeschickt |
lastPolledAt | auto | DateTime | Letzter Status-Poll |
lastTrackingPolledAt | auto | DateTime | Letzter Tracking-Poll (nur bei Einschreiben) |
trackingEvents[] | auto | JSON[] | Rohe Tracking-Events der Deutschen Post |
deliveredAt | auto | DateTime | Zustellung (nur Einschreiben) |
returnReceiptAt | auto | DateTime | Rückschein-Eingang (nur Einschreiben mit Rückschein) |
finalizedAt | auto | DateTime | Wann der Status final wurde (sent / failed / cancelled) |
Status-Lebenszyklus
Terminale Status: sent, failed, cancelled — sobald erreicht, wird finalizedAt gesetzt und der Brief ist nicht mehr locked.
DocuGuide-Integration
Authentifizierung
POST /api/LoginmitvendorID,ekp(Postkundennummer),secret,password- JWT-Token gültig 60 Minuten — in Redis gecacht mit TTL 55 Minuten
- Automatischer Refresh bei 401-Response
Submit-Aufruf
epost-submit-worker.ts (BullMQ, Concurrency 2, Retries 3× mit exponential backoff 5s/10s/20s) sendet:
POST /api/Letter
{
"fileName": "Brief-12345.pdf",
"data": "<base64-encoded-PDF/A-1b>",
"isColor": false,
"isDuplex": true,
"testFlag": false,
"registeredLetter": "Einschreiben Rückschein",
"custom1": "<letter-id>",
"addressLine1": "Max Mustermann",
"zipCode": "10115",
"city": "Berlin",
"country": "DE"
}
DocuGuide antwortet mit { letterID: "DOC-..." } — landet in epostLetterID.
Status-Polling
Periodischer Job ruft GET /api/Letter/:epostLetterID und mappt:
| DocuGuide-Code | Bedeutung | LetterSubmission.status |
|---|---|---|
1 | Eingangsprüfung läuft | submitted |
2 | In Bearbeitung | in_progress |
3 | Druck fertig, versandfertig | ready_to_send |
4 | Physisch versendet | sent |
99 | Fehler | failed |
Tracking (nur Einschreiben)
Wenn registeredLetter ≠ none: zusätzlicher Tracking-Poll getLetterTracking() → trackingEvents[], deliveredAt, returnReceiptAt.
Pricing
Die Aufschläge pro Einschreiben-Variante (zusätzlich zum DocuGuide-Grundpreis, netto):
| Variante | Aufschlag | Wann nutzen? |
|---|---|---|
none (Standard-Brief) | 0,00 € | Normale Korrespondenz, Standard-Post |
einwurf_einschreiben | +2,35 € | Nachweis, dass Brief eingeworfen wurde — z. B. Kündigungen mit Frist |
einschreiben (Standard) | +2,65 € | Nachweis der Übergabe — Empfänger oder Bevollmächtigter quittiert |
einschreiben_rueckschein | +4,85 € | Wie Einschreiben + Rückschein an Absender — z. B. juristische Schriftstücke, gerichtsverwertbar |
epostPricing.service.ts rechnet das im Submit-Dialog inline aus, damit der User vor dem Klick weiß, was er bezahlt.
Submit-Dialog (FE)
SpeamPostSubmitDialog.tsx — der zentrale UX-Schritt vor dem Versand:
- Druck-Optionen — Toggle
isColor, ToggleisDuplex - Einschreiben-Variante — Radio über die vier Optionen (mit Aufpreis-Anzeige)
- Attachments — Picker für
Document-Einträge, die hinten an den Brief gemerged werden - Inline-Pricing — Aufpreis-Summe, basierend auf den gewählten Optionen
- Submit-Button — feuert
POST /letters/:id/submit, schließt den Dialog mit202 Accepted-Toast
Anschließend wird der Brief locked und ist nicht mehr editierbar. Die SpeamPostSection-Komponente am Brief-Detail zeigt live den Submission-Status mit Spinner / Badge / Tracking-Panel.
Storno
POST /letters/:id/submissions/:submitId/cancel — funktioniert nur vor terminalem Status. Konkret:
- ✅
queued→cancelled - ✅
submitted→cancelled(DocuGuide kennt den Brief schon, aber er ist noch nicht im Druck) - ❌
in_progress/ready_to_send— DocuGuide druckt bereits, kein Storno mehr möglich - ❌
sent— physisch versendet, definitiv weg
Bei erfolgreichem Cancel wird finalizedAt gesetzt, der Brief verliert das Lock — eine neue Submission kann gestartet werden.
Replay-Schutz
pdfHash (SHA-256 über das fertige PDF) verhindert versehentliches Doppel-Versenden. Wenn der gleiche Brief mit identischem Inhalt zweimal submitted wird, erkennt der Service das und gibt eine Warnung statt eines weiteren Submits.
API/Schnittstellen
| Methode | Endpoint | Zweck | CASL |
|---|---|---|---|
POST | /api/letters/:id/submit | Versand starten (202 Accepted, async Worker) | do SubmitLetter |
GET | /api/letters/:id/submissions | Submission-Historie | view LetterSubmission |
GET | /api/letters/:id/submissions/:submitId | Detail einer Submission inkl. Tracking | view LetterSubmission |
POST | /api/letters/:id/submissions/:submitId/cancel | Abbrechen (vor terminalem Status) | do SubmitLetter |
Verknüpfungen
- Briefe (Letter) — Hauptmodul, das den Brief-Inhalt verwaltet
- Brief vs. Mail vs. ePost (Konzept) — Abgrenzung der drei Welten
- Dokumente — Quelle für
attachmentDocumentIds[]
Häufige Fehler und Lösungen
| Fehler | Lösung |
|---|---|
Submit endet sofort in failed | DocuGuide-Token abgelaufen oder Credentials fehlen. errorDetail enthält die API-Antwort — bei 401 Admin informieren. |
| Submit-Button ist disabled | Brief hat keinen Empfänger oder Adress-Snapshot ist unvollständig (parentZip leer). Brief-Detail prüfen. |
Versand hängt auf submitted | DocuGuide-Status-Polling wirft Fehler — Worker-Log prüfen. Manueller Status-Pull über Detail-Seite. |
| Tracking-Events erscheinen nicht | Nur bei Einschreiben-Varianten. Bei registeredLetter = none gibt es keine Sendungsverfolgung. |
| Cancel ist disabled | Submission ist bereits im Druck (in_progress / ready_to_send) oder versendet (sent). Nicht stornierbar. |
| Pricing-Aufpreis stimmt nicht | Werte in epostPricing.service.ts — bei DocuGuide-Tarifänderung aktualisieren. |
Versionshinweise
- 2026-05-26 (Welle 148): Initiale Veröffentlichung. Quelle: BE-Commits
5de6fbb3(DocuGuide-Integration),b8096e48(Response-Shape-Fix),34487c76(Tracking + Mail-Scan-Integration). FE-Commitsd5bdc79f(FE-Integration),aefa838f(Lock-Banner-Fix),f73f6286(Einschreiben-Dialog). Workerepost-submit-worker.ts(Concurrency 2, Retries 3×).