Recurring Document Flow
Recurring agreements, periodic occurrences, the Hangfire engine, pre-checks, review queue and how generated documents join the AR / ETA / GL cycle.
Recurring Document Flow
A recurring agreement describes a customer or supplier arrangement that produces documents on a schedule: monthly support fees, quarterly licences, weekly deliveries, a monthly rent or hosting bill from a vendor. The recurring engine materialises one occurrence per period, runs the pre-checks, and generates the real business documents — on the customer side a sales order and delivery note or an AR invoice directly; on the supplier side a purchase order, a draft goods receipt and an AP bill — through the same handlers a person would use. Nothing is booked twice: every period has one business key and the engine checks it before it acts.
Hub: Target Document Flow. Neighbours: Sales, Accounts Receivable, Purchasing, Accounts Payable, ETA.
Screens: /sales/recurring (operations dashboard; ?direction=Customer|Supplier scopes the view), /sales/recurring/agreements and /sales/recurring/occurrences (customer), /pc/recurring/agreements and /pc/recurring/occurrences (supplier).
#Chain overview
Agreement → occurrence → documents → ledgers
flowchart LR RA[Recurring Agreement] --> OC[Occurrence per period] OC -->|Customer · InvoiceOnly| AR[AR Invoice] OC -->|Customer · OrderAndDelivery| SO[Sales Order] SO --> DN[Delivery Note] DN --> AR AR --> GL[GL Journal] AR --> ETA[ETA e-invoice] OC -->|Supplier · InvoiceOnly| AP[AP Bill] OC -->|Supplier · PurchaseOrderAndReceipt| PO[Purchase Order] PO --> GRN[Goods Receipt · draft] PO --> AP GRN -.->|posted by operator| GL AP -->|approved| GL class RA egkDraft class OC,SO,DN,PO,GRN egkActive class AR,AP,GL egkSuccess class ETA egkWarn
Recurring agreement (RecurringAgreementStatus)
#Statuses
Draft, Active, Suspended, Cancelled, Expired
Agreement lifecycle
flowchart TB Start((Create)) --> Draft[Draft] Draft -->|Activate| Active[Active] Active -->|Suspend| Suspended[Suspended] Suspended -->|Resume| Active Active -->|Cancel| Cancelled[Cancelled] Active -->|End date reached| Expired[Expired] Active -->|Auto-renew| Active class Start egkStart class Draft egkDraft class Active egkSuccess class Suspended egkWarn class Cancelled egkError class Expired egkTerminal
#Header fields that drive generation
| Field | Effect |
|---|---|
| Direction | Customer (AR side: sales order / delivery / AR invoice) or Supplier (AP side: purchase order / goods receipt / AP bill) |
| Fulfillment mode | Customer: InvoiceOnly → AR invoice per period; OrderAndDelivery → sales order → delivery → AR invoice. Supplier: InvoiceOnly → AP bill per period; PurchaseOrderAndReceipt → purchase order → draft goods receipt → AP bill |
| Recurrence rule | Frequency (weekly, monthly, quarterly, yearly, custom days) and anchor day |
| Billing timing | InAdvance (run date = period start) or InArrears (run date = period end) |
| Start / end date, auto-renew, renewal term | Term of the agreement; the scheduler never creates a period after the end date |
| Grace days | How late an automatic run may still generate the period; later periods expire (a manual run ignores the window) |
| Company, branch, partner branch | Stamped on every generated document (ledger and ETA issuer) |
| Warehouse, price list, payment terms | Defaults for order-and-delivery agreements; on the supplier side the warehouse decides whether a purchase order and goods receipt are raised (stock item) or only the bill (service) |
| Auto post | Customer: post the AR invoice immediately (GL + ETA) or leave it as a draft for review. Supplier: submit the AP bill into the approval workflow and record the agreement owner as the purchase-order approver; never posts the goods receipt |
An agreement needs at least one line to activate. Lines carry description, quantity, unit price, optional catalog product, discount, and ETA-aware tax rows (fractions, e.g. 0.14).
#Actions
| Action | Availability | Behavior |
|---|---|---|
| Activate | Draft, Suspended |
Aligns the scheduling cursor to the anchor day and starts materialising periods |
| Suspend | Active |
Open periods stay Scheduled but are parked as PendingReview when due |
| Resume | Suspended |
Continues from the cursor; no back-generation of skipped periods without a manual run |
| Cancel | any except terminal | Open periods are Expired at their next run |
| Schedule now | Active |
Materialises periods up to the horizon (default 30 days) |
Occurrence (RecurringOccurrenceStatus)
#Statuses
Scheduled, Due, Processing, Generated, Submitted, Completed, Failed, PendingReview, Cancelled, Expired
Occurrence lifecycle — lease, pre-checks, generate
flowchart TB Sch[Scheduled] -->|Run date reached| Due[Due] Due -->|Lease claimed| Proc[Processing] Proc -->|Pre-check blocks| Failed[Failed] Proc -->|Needs a person| Review[PendingReview] Proc -->|Documents created| Gen[Generated] Gen -->|ETA accepted or no bridge| Done[Completed] Gen -->|ETA submitted| Sub[Submitted] Sub --> Done Failed -->|Retry window| Due Review -->|Run manually| Proc Due -->|Grace elapsed| Exp[Expired] Sch -->|Agreement cancelled| Can[Cancelled] class Sch egkDraft class Due,Proc,Gen,Sub egkActive class Done egkSuccess class Review egkWarn class Failed egkError class Exp,Can egkTerminal
#Business key and idempotency
Every occurrence carries IdempotencyKey = REC:{agreementId}:{periodStart:yyyyMMdd}. The key is unique in the database and is handed to the AR invoice factory or the AP bill factory (ExternalIdempotencyKey) or stamped as the sales-order Reference; supplier purchase orders use the deterministic number RPO-{agreement}-{yyyyMMdd}. A replayed job, a second worker, or a double click therefore finds the existing documents and links them instead of generating again. An occurrence that already has an AR or AP invoice is closed as already generated without touching the ledgers.
#Pre-checks (before any document is created)
| Code | Outcome | Meaning |
|---|---|---|
CompanyMissing |
Failed | No company on the agreement and the tenant has zero or several active companies |
AgreementInactive, PeriodOutsideTerm, InvalidPeriod, NoLines |
Failed | Structural problems; retrying will not help |
InvalidTaxRate, InvalidCurrency, InvalidExchangeRate |
Failed | Tax rates must be fractions, currency a 3-letter ISO code |
PartnerMissing, PartnerInactive, PartnerRoleMissing, PartnerBranchMismatch |
Failed | Partner state |
CreditHold, ZeroPrice, ProductInactive, MaterialMissing, StockCheckFailed |
PendingReview | A person decides |
InsufficientStock |
PendingReview | Order-and-delivery only: ATP in the agreement warehouse is below the period quantity |
WarehouseMissing, WarehouseInactive, ProductMissing |
Failed | Order-and-delivery master data |
A credit-limit refusal raised by the AR factory during generation is also parked as PendingReview.
#What gets generated
| Mode | Steps (each idempotent) | Documents linked on the occurrence |
|---|---|---|
InvoiceOnly |
AR invoice via the AR invoice factory with the occurrence key; posted when Auto post is on | ArInvoiceId |
OrderAndDelivery |
Sales order (Reference = key) → Confirm → Delivery note → Ship → AR invoice from delivery (DLV:{deliveryId}) |
SalesOrderId, DeliveryNoteId, ArInvoiceId |
Supplier InvoiceOnly |
AP bill via the AP bill factory with the occurrence key; submitted for approval when Auto post is on, otherwise Draft | ApInvoiceId |
Supplier PurchaseOrderAndReceipt |
Purchase order RPO-{agreement}-{yyyyMMdd} (Submitted; Approved when Auto post is on) → draft goods receipt against it → AP bill linked to both. Without a warehouse (service) only the bill is raised |
PurchaseOrderId, GoodsReceiptId, ApInvoiceId |
The generated AR invoice then follows the normal cycle: ArInvoiceSentEvent → GL journal (Dr AR control / Cr revenue / Cr tax), ArInvoicePostedIntegrationEvent → ETA e-invoice draft when the company has an enabled ETA bridge setting. ETA status events (Valid, Cancelled, Rejected) close or flag the occurrence through the AR ↔ ETA sync.
On the supplier side the engine never books stock or a liability by itself: the goods receipt stays Draft until Purchasing posts it on physical receipt (that posting moves stock and raises the GRN journal), and the AP bill goes through the normal AP approval before its journal (Dr expense or GR/IR / Cr AP control) is posted. The occurrence is Completed once the three documents exist; the open receipt shows in the Purchasing worklists, not in the recurring review queue.
#Actions
| Action | Availability | Behavior |
|---|---|---|
| Run now | Scheduled, Due, Failed, PendingReview |
Manual execution; ignores the grace window and takes over an expired lease |
| Review → resolve | PendingReview |
Fix data (stock, credit, prices) then Run now |
| Cancel period | Scheduled, Due, PendingReview, Failed |
Period will never be generated |
| View documents | Generated and later |
Opens the sales order / delivery / AR invoice / e-invoice, or the purchase order / goods receipt / AP bill |
Engine, jobs and safety
- Scheduler job (
ScheduleRecurringOccurrencesCommand): materialises periods up toScheduleHorizonDaysfor every active agreement in every tenant. - Processor job (
ProcessDueRecurringOccurrencesCommand): claims due occurrences with a database lease (ExecuteUpdate,LeaseMinutes, worker id), executes them one by one, writes aRecurringExecutionLogrow per attempt and releases the lease. A crashed worker's lease expires and another worker takes over — never generating twice because the business key is checked first. - Retry:
Failed (retryable)occurrences are picked up again after the retry delay;Failed (permanent)andExpiredare not. - Notifications: failures, insufficient stock, expired periods and ETA refusals raise operator notifications; the dashboard at
/sales/recurringshows due / processing / review / failed counters, next runs and the last execution log per agreement. - Multi-tenant: jobs fan out per tenant (
TenantFanOutJob); every command runs inside the tenant's scope and company. - Legacy AR recurring templates were migrated into agreements (
MigrateArRecurringTemplates); the old template screen remains read-only.
#Post-action sync
The agreement and occurrence lists reload after every action and show the engine's status, last message and linked documents. A failed run leaves the previous status, records the attempt in the execution log and (for retryable failures) schedules the next attempt.
#Scenarios
| Scenario | Outcome |
|---|---|
| Monthly support, invoice only, ETA bridge enabled | AR invoice posted, GL journal, ETA draft; occurrence Generated until ETA reports Valid → Completed |
| Same period processed twice (job replay) | Second run reports documents already generated; no new invoice, journal or e-invoice |
| Order-and-delivery with stock below quantity | PendingReview with InsufficientStock; after stock arrives, Run now generates order → delivery → invoice once |
| Customer on credit hold | PendingReview with CreditHold; nothing invoiced |
| Automatic run later than grace days | Expired; a manual run can still generate the period |
| Agreement cancelled with open periods | Open periods Expired at their next run |
| Monthly hosting bill from a vendor (supplier, invoice only) | AP bill keyed by the period; approval and the AP journal follow the normal AP flow |
| Weekly stock delivery from a vendor (supplier, PO and receipt, warehouse set) | Purchase order + draft goods receipt + linked AP bill in one run; the storekeeper posts the receipt when the goods arrive — only then does stock move |
| Worker crashes after the purchase order was created | The retry finds RPO-… for the period, reuses it, and continues with the receipt and the bill — no second PO |