Zum Inhalt

Handler und Lebenszyklus

Wie du Dokumente über die Handler lädst, Stages ausführst und den Zustand am Handler liest.

DocumentHandler — Lebenszyklus-Abstraktion

DiWaSS-Dokumente durchlaufen mehrstufige Lebenszyklen mit serverseitiger Versionierung und Validierung. Statt das im Anwendungs-Code abzubilden, kapselt pro Dokumenttyp ein Handler den Zyklus. client.DocumentHandler ist die Factory:

Factory-Methode Handler Ablage in ZEDAL
CreateNotificationHandler() NotificationDocumentHandler — Notifizierung aus Unternehmenssicht Nachweis-Bereich (Akte)
CreateMovementHandler() MovementDocumentHandler — Begleitformular aus Unternehmenssicht Versandpapier-Bereich
CreateAnnex7Handler() Annex7DocumentHandler — Annex-VII-Dokument Versandpapier-Bereich
CreateAuthorityNotificationHandler() AuthorityNotificationDocumentHandler — Notifizierung aus Behördensicht Nachweis-Bereich (Akte)
CreateAuthorityMovementHandler() AuthorityMovementDocumentHandler — Begleitformular aus Behördensicht Nachweis-Bereich (Akte)
CreateBmuHandler() BmuDocumentHandler — BMU-Begleitschein und -Übernahmeschein, kein DiWaSS eigener Weg (siehe unten)
CreateInboxImporter() IInboxImporter — Eingänge übernehmen —

Was die Speicherbereiche unterscheidet, steht unter Speicherbereiche.

BmuDocumentHandler ist kein DiWaSS-Handler

Er erbt nicht von DiwassDocumentHandlerBase und hat deshalb weder SendToDiwass() noch WaitForDiwassTransfer() noch FollowupIntents. Sein Weg heißt SubmitTransportDocumentToZedal(...), und geladen wird über LoadTransportDocumentFromZedal(docIdentifier, value). Wer beide Welten bedient, kann die Handler nicht über einen gemeinsamen Basistyp behandeln. Seine Methoden stehen unter BmuDocumentHandler.

Ein bestehendes Dokument laden

Hier verwechseln sich zwei Dinge, die gleich klingen und verschiedene Kennungen brauchen:

Aufruf Parameter Wann
LoadFromZedal(DocumentId docId) ZEDAL-Dokument-Id (DocumentId) Du hast das Dokument in dieser Anwendung schon einmal gespeichert oder geladen und die Id behalten (handler.ZedalDocumentId), oder du hast sie aus einer Inbox-/Query-Antwort.
LoadFromZedalByNotificationId(string) fachliche Notifizierungsnummer Du kennst nur die Nummer aus dem Vorgang. Nur auf NotificationDocumentHandler.
LoadFromZedalByMovementId(string) fachliche Movement-Kennung Nur auf MovementDocumentHandler.
LoadFromZedalByAnnex7DocumentNo(string) Annex-VII-Dokumentnummer Nur auf Annex7DocumentHandler.
ReloadFromZedal() — Lädt die aktuellste Version derselben Id. Verlangt eine gesetzte ZedalDocumentId, sonst InvalidOperationException.

DocumentId ist ein generierter Protobuf-Typ, kein String. Ein LoadFromZedal("WSR/DE/…") kompiliert nicht — dafür sind die …By…-Varianten da.

Die …By…-Varianten lösen die fachliche Kennung über die Dokumentlisten der QueryApi auf und delegieren dann an LoadFromZedal. Zwei Verhaltensweisen sind wichtig:

  • Kein Treffer → KeyNotFoundException mit der gesuchten Kennung im Text. Das ist der Normalfall, wenn ein Fremd-Update noch nicht im ZEDAL-Bestand angekommen ist — Fremdänderungen brauchen zwei bis drei Minuten und den Inbox-Import, siehe Aufrufe und Synchronisation.
  • Mehrere Treffer → geladen wird defensiv das Dokument mit dem jüngsten last_changed.

Zustand am Handler

Die Stage- und Sende-Methoden geben nichts zurück. Alles Weitere liest du an Properties ab:

Property Inhalt
DocXml Aktueller Dokumentstand als ZeWaSS-XML (byte[])
FollowupIntents Mögliche nächste Aktionen im regulatorischen Lebenszyklus. Property, keine Methode
ValidationErrors / HasValidationErrors Validierungsfehler der letzten Mutation
TransferHistory Versand-Historie, neuester Eintrag zuerst
HasError / LastError Bezieht sich auf TransferHistory[0], also den jüngsten Versuch
ZedalDocumentId Id im ZEDAL-Dokumentenspeicher
TargetFileId Akte, in der das Dokument liegt. null bei Ablage im Versandpapier-Bereich
LastTransferLogId Log-Id des letzten Versands; Schlüssel für das Status-Polling
DiwassNotificationId Von DiWaSS zugewiesene Kennung, sobald der Versand synchronisiert ist

Zustand nie über Listenlängen bestimmen

ValidationErrors wird von Refresh() nicht zurückgesetzt, weil die Get-Endpunkte keine Validierungsfehler liefern — eine gefüllte Liste kann also von einer früheren Mutation stammen. TransferHistory füllt sich nur über Refresh(), nicht über Mutationen. Und die Antwort auf eine Behörden-Nachforderung landet in der requestInformation-Liste, während die Antwortliste leer bleibt.

Werte deshalb FollowupIntents aus, nicht die Länge einzelner Listen. Details unter Lebenszyklus → Zustand bestimmen.

Der Weg durch eine Stage

var handler = client.DocumentHandler.CreateNotificationHandler();

await handler.CreateSubmission(submission);            // SubmissionDocumentType

await handler.SendToDiwass(
    ZedalCallContext.Default.WithTimeout(180));        // budget per operation, see Lifecycle

await handler.WaitForDiwassTransfer(
    timeout:      TimeSpan.FromMinutes(10),
    pollInterval: TimeSpan.FromSeconds(15));

await handler.ReloadFromZedal();                       // only then do server fields appear in DocXml

if (handler.HasValidationErrors)
{
    foreach (var error in handler.ValidationErrors)
    {
        Console.WriteLine(error);
    }
}

Vier Eigenschaften dieses Ablaufs, die man kennen muss:

  • SendToDiwass() speichert implizit. Ist das DocXml seit dem letzten Abgleich verändert oder lag es noch nie in ZEDAL, ruft SendToDiwass() selbst SaveToZedal() auf. Ein explizites SaveToZedal() vorher ist erlaubt und wird erkannt — sinnvoll, wenn du die ZedalDocumentId vor dem Push persistieren willst, etwa zur Wiederaufnahme nach einem Absturz.
  • SaveToZedal() kommuniziert nicht nach außen. Es persistiert nur in ZEDAL. Optional nimmt es eine bestehende Akte (targetFile) — das greift nur beim Erst-Save eines Dokuments, das im Nachweis-Bereich landet. Ohne Angabe entsteht eine neue Akte.
  • WaitForDiwassTransfer lädt das Dokument nicht neu. Es pollt nur den Übermittlungsstatus. Wer auf serverseitig ergänzte Felder zugreifen will — etwa die zugewiesene Notifizierungsnummer —, muss danach ReloadFromZedal() aufrufen.
  • Das Poll-Intervall hat eine Untergrenze. MinTransferPollInterval ist 1 Sekunde; ein kleinerer Wert wirft ArgumentOutOfRangeException, bevor gepollt wird. In der Praxis sind 5 bis 15 Sekunden angemessen.

WaitForDiwassTransfer kehrt bei RECEIVED_RECEIPT_POSITIVE und DELIVERED_TO_RECIPIENT_INBOX regulär zurück, wirft DiwassTransferFailedException bei RECEIVED_RECEIPT_NEGATIVE und CANCELLED und TimeoutException, wenn der Status nicht rechtzeitig terminal wird. Die Bedeutung dieser Zustände steht unter Versandverfahren.

Für alles, was kein Übermittlungsstatus ist, gibt es WaitUntil:

await handler.WaitUntil(
    condition:    h => h.FollowupIntents.Count > 0,
    timeout:      TimeSpan.FromMinutes(10),
    pollInterval: TimeSpan.FromSeconds(15));

WaitUntil lädt das Dokument bei jedem Durchlauf neu (ReloadFromZedal) und prüft dann die Bedingung. Die Deadline aus dem ZedalCallContext gilt pro Reload, nicht für die Gesamtwartezeit — die steuert timeout. Der CancellationToken beendet auch die Wartezeit zwischen zwei Reloads, ein Abbruch hängt also nicht bis zum Ende des Intervalls.

Das Zeitbudget muss zu dem passen, worauf du wartest. Für die Rückmeldung des EU-Systems auf einen eigenen Vorgang sind Minuten realistisch — Richtwerte unter Versandverfahren. Auf eine Behördenentscheidung wartet man dagegen nicht in einer Schleife: Das ist ein Vorgang mit menschlicher Beteiligung und gehört in den periodischen Abgleich, nicht in ein Timeout.

Warten ersetzt kein Abholen

Beide Warteschleifen laufen im SDK, nicht am Dokument. Sie sagen nichts darüber, ob eine fachliche Antwort aus dem EU-System schon da ist — die kommt asynchron als neue Dokumentversion, und für Fremdänderungen ist der Inbox-Import der Zustellweg. Ein Polling ohne Import pollt gegen einen Stand, der sich nie ändert. Siehe Aufrufe und Synchronisation.

Welche Stage-Methode in welcher Reihenfolge zulässig ist, steht in den Anleitungen: Notifizierung (Unternehmen), Begleitformular (Unternehmen), Behördensicht.

Movement-Sequenznummern

Die Nummernvergabe liegt unter client.DocumentNumbers:

var next = await client.DocumentNumbers.GetNextMovementSequenceNumber(notificationId);

Die Nummer wird nicht serverseitig reserviert: Zwei Prozesse, die gleichzeitig fragen, bekommen dieselbe. Mehrere aufeinanderfolgende Nummern holt GetNextMovementSequenceNumbers(notificationId, count) in einer Abfrage. Für den Offline-Fall gibt es MovementSequenceNumber.NextFrom(IEnumerable<string>) — dieselbe Rechnung über eine selbst geführte Liste. Formate und Kontingentregel stehen unter Begleitformular (Unternehmen).