Skip to content

AI translation

This page was translated from German with AI. If in doubt, the German original applies.

Handlers and lifecycle

How to load documents via the handlers, run stages and read the state at the handler.

DocumentHandler — lifecycle abstraction

DIWASS documents go through multi-stage lifecycles with server-side versioning and validation. Instead of modeling this in application code, one handler per document type encapsulates the cycle. client.DocumentHandler is the factory:

Factory method Handler Storage in ZEDAL
CreateNotificationHandler() NotificationDocumentHandler — notification from the company perspective File area (Nachweis-Bereich), in a file
CreateMovementHandler() MovementDocumentHandler — movement document from the company perspective Transport paper area (Versandpapier-Bereich)
CreateAnnex7Handler() Annex7DocumentHandler — Annex VII document Transport paper area
CreateAuthorityNotificationHandler() AuthorityNotificationDocumentHandler — notification from the authority perspective File area, in a file
CreateAuthorityMovementHandler() AuthorityMovementDocumentHandler — movement document from the authority perspective File area, in a file
CreateBmuHandler() BmuDocumentHandler — BMU consignment note (Begleitschein) and acceptance note (Übernahmeschein), not DIWASS separate path (see below)
CreateInboxImporter() IInboxImporter — import incoming documents —

What distinguishes the storage areas is explained under Storage areas.

BmuDocumentHandler is not a DIWASS handler

It does not inherit from DiwassDocumentHandlerBase and therefore has neither SendToDiwass() nor WaitForDiwassTransfer() nor FollowupIntents. Its path is SubmitTransportDocumentToZedal(...), and loading is done via LoadTransportDocumentFromZedal(docIdentifier, value). If you serve both worlds, you can’t handle the handlers through a common base type. Its methods are listed under BmuDocumentHandler.

Loading an existing document

Two things that sound alike but need different identifiers are easily confused here:

Call Parameter When
LoadFromZedal(DocumentId docId) ZEDAL document ID (DocumentId) You have already saved or loaded the document in this application and kept the ID (handler.ZedalDocumentId), or you got it from an inbox/query response.
LoadFromZedalByNotificationId(string) business notification number You only know the number from the case. Only on NotificationDocumentHandler.
LoadFromZedalByMovementId(string) business movement identifier Only on MovementDocumentHandler.
LoadFromZedalByAnnex7DocumentNo(string) Annex VII document number Only on Annex7DocumentHandler.
ReloadFromZedal() — Loads the latest version of the same ID. Requires ZedalDocumentId to be set, otherwise InvalidOperationException.

DocumentId is a generated Protobuf type, not a string. LoadFromZedal("WSR/DE/…") does not compile — that’s what the …By… variants are for.

The …By… variants resolve the business identifier via the query index and then delegate to LoadFromZedal. Two behaviors are important:

  • No match → KeyNotFoundException with the searched identifier in the message. This is the normal case when an external update has not yet arrived in the ZEDAL data store — external changes take 100 to 150 seconds and the inbox import, see Calls and synchronization.
  • Multiple matches → as a defensive measure, the document with the most recent last_changed is loaded.

State on the handler

The stage and send methods return nothing. You read everything else from properties:

Property Content
DocXml Current state of the document as ZeWaSS XML (byte[])
FollowupIntents Possible next actions in the regulatory lifecycle. A property, not a method
ValidationErrors / HasValidationErrors Validation errors of the last mutation
TransferHistory Transfer history, newest entry first
HasError / LastError Refers to TransferHistory[0], i.e. the most recent attempt
ZedalDocumentId ID in the ZEDAL document store
TargetFileId File in which the document is stored. null when stored in the transport paper area
LastTransferLogId Log ID of the last transfer; key for status polling
DiwassNotificationId Identifier assigned by DIWASS once the transfer is synchronized

Never determine state from list lengths

ValidationErrors is not reset by Refresh(), because the get endpoints don’t return validation errors — so a populated list may come from an earlier mutation. TransferHistory is only populated via Refresh(), not via mutations. And the response to a request for additional information from the authority ends up in the requestInformation list, while the response list stays empty.

Therefore evaluate FollowupIntents, not the length of individual lists. Details under Lifecycle → Determining the state.

The path through a 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);
    }
}

Four characteristics of this flow that you need to know:

  • SendToDiwass() saves implicitly. If the DocXml has changed since the last synchronization or has never been in ZEDAL, SendToDiwass() calls SaveToZedal() itself. An explicit SaveToZedal() beforehand is allowed and is detected — useful if you want to persist the ZedalDocumentId before the push, for example to resume after a crash.
  • SaveToZedal() does not communicate externally. It only persists in ZEDAL. Optionally, it takes an existing file (targetFile) — this only applies to the first save of a document that ends up in the file area. Without it, a new file is created.
  • WaitForDiwassTransfer does not reload the document. It only polls the transfer status. If you want to access fields added on the server — such as the assigned notification number — you must call ReloadFromZedal() afterwards.
  • The poll interval has a lower limit. MinTransferPollInterval is 1 second; a smaller value throws ArgumentOutOfRangeException before any polling happens. In practice, 5 to 15 seconds is appropriate.

WaitForDiwassTransfer returns normally on RECEIVED_RECEIPT_POSITIVE and DELIVERED_TO_RECIPIENT_INBOX, throws DiwassTransferFailedException on RECEIVED_RECEIPT_NEGATIVE and CANCELLED, and throws TimeoutException if the status does not become terminal in time. The meaning of these states is explained under Sending procedure.

For anything that is not a transfer status, there is WaitUntil:

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

WaitUntil reloads the document on every iteration (ReloadFromZedal) and then checks the condition. The deadline from the ZedalCallContext applies per reload, not to the total wait time — that is controlled by timeout. The CancellationToken also ends the wait between two reloads, so a cancellation does not hang until the end of the interval.

The time budget must match what you are waiting for. For the EU system’s response to your own operation, minutes are realistic — guideline values under Sending procedure. An authority decision, on the other hand, is not something you wait for in a loop: it is a process involving people and belongs in the periodic synchronization, not in a timeout.

Waiting is no substitute for fetching

Both wait loops run in the SDK, not on the document. They say nothing about whether a business response from the EU system has already arrived — it arrives asynchronously as a new document version, and for external changes the inbox import is the delivery path. Polling without an import polls against a state that never changes. See Calls and synchronization.

Which stage method is allowed in which order is described in the guides: Notification (company), Movement document (company), Authority perspective.

Movement sequence numbers

MovementDocumentHandler comes with number assignment:

var next = await handler.GetNextSequenceNumber(notificationId);

The number is not reserved on the server: two processes that ask at the same time get the same number. For the offline case, there is MovementDocumentHandler.NextSequenceFrom(IEnumerable<string>) — the same calculation over a list you maintain yourself. Formats and the rule for the intended number of shipments are described under Movement document (company).