ZuloOne
English
Sign in
Menu

Enqueue an outbound call when a document posts

Updated

After GoodsIssue or an invoice posts, send it to a WMS or an operator. The outbox row is written in the same transaction as posting. The host sends later. A job after the fact is the wrong door — Poll a REST API from a job.

Note

Never HttpClient from a script. IOutboundGateway.EnqueueAsync by channel name. The host owns URL, secret, signature, retry, and SSRF — Send an outbound call, What a script can do.

How to#

1. Enqueue from the posting path. OnAfterPostAsync runs inside the transition: movements, subtype, and audit are still uncommitted. A thrown handler rolls the post back, and a rolled-back post sends nothing. GetTransactions stays a synchronous movement calculation — Write postings. Enqueue is orchestration: a service the event (or another posting-path caller) invokes is fine; the EnqueueAsync itself must stay on that path, not on a later job.

OnAfterPostAsync can run twice (repost, a totals driver under the same doc id). IdempotencyKey is the guard: the unique index is (Channel, IdempotencyKey). Same key, same payload — the existing row. Same key, different payload — throw. Derive the key from the document, not from the clock.

// Example only — channel `wms-ship` and IWmsShip are invented.
// Inventory.GoodsIssue has no WMS enqueue today.
await ScriptServices.Get<IOutboundGateway>().EnqueueAsync(new OutboundRequest
{
    Channel = "wms-ship",
    IdempotencyKey = "wms:" + document.MetaId.ToString("N"),
    Payload = body,
    SourceTable = "GoodsIssue",
    SourceRecordId = document.MetaId,
});

The working pattern in the tax packs: OnAfterPostAsync on SalesRealization calls ISaudiEInvoice.EnsureForInvoiceAsync / IUaEInvoice.EnsureForInvoiceAsync, and that service enqueues (fatoora-clearance, erpn-send). EnqueueAsync only writes a row. OutboundSenderService delivers it outside the transaction so DeadlockRetry cannot replay an HTTP POST.

2. Owner vs wrap. The handler in the document's own model is link 0 and does not call next. A handler on a foreign document must declare [ExtensionOf("GoodsIssue")] and call next from every override — Write an event handler, Wrap with Chain of Command. Missing [ExtensionOf] is ZOCOC004; missing next is ZOCOC001.

[ExtensionOf("GoodsIssue")]
public partial class GoodsIssueWmsEventHandler : TypedDocumentEventHandler<GoodsIssue>
{
    public override async Task<EventResult> OnAfterPostAsync(
        GoodsIssue header, EventContext context)
    {
        var prior = await next(header, context);
        if (!prior.Success) return prior;
        await context.GetService<IWmsShip>().EnqueueIssueAsync(header);
        return EventResult.Ok();
    }
}

Example only — there is no GoodsIssueWmsEventHandler in Inventory. Work after next() sees stock and GL.

3. Channel in your model (OutboundChannels/<name>.channel.json), bump your modelVersion in the same change, and POST /api/services/rebuild for a new channel — Bump the model version.

Warning

A successful send is not a receipt. Ask whether the response is the other side's decision, or "accepted for processing". Closing the document on Succeeded when a receipt still has to be fetched registers work the other system has not seen.

InDoubt means the request left and no answer came back. The document may already be registered. Do not retry that submit. Query fate with IOutboundCall; do not enqueue a second submit.

EnqueueAsync opens its own connection. Call it from a posting event or a command — not from an after-insert that already read another table (that second connection promotes the scope to MSDTC).

Signing, four guards against a second submit, TakeCompletedAsync: Outbound integrations. Door table: Integrate with a third-party REST API.

What's not there yet#

The platform does not accept an inbound callback that completes the send. CompletionHandler is reserved and unread. Drain your own outbox for send outcomes; poll the other side when you need a receipt — Poll a REST API from a job. An integration test cannot cover the HTTP exchange: cover everything before the enqueue and everything after the response; say so in the test header.

Where next#

Was this page helpful?