Finance — Architecture & data
Main flow
Billing generation pipeline
Billing generation runs in 3 separate layers (invariant INV-BILLING-CORE-001):
- Trigger: real confirm, subscription approve/skip, regenerate when editing a confirmed booking.
- Gate:
UpdateBillingByIdAsyncor an equivalent gate is a mandatory condition before finalizing the booking (INV-CONFIRM-BILLING-001). - Frequency override:
ResolveBillingFrequencyOverridemaps Pay Now →One_Off, Direct Debit →Weekly; the real path and preview share the helper, the preview does not mutate the billing row. - Deposit: confirming with a deposit still generates an invoice for the full amount; settlement sits outside billing generation (INV-DEP-015).
- Subscription: approve/skip generates/reverses invoices through the backend service; an active price line is a mandatory condition to persist the invoice (INV-SUB-BM-04).
Key boundaries
| Boundary | Rule |
|---|---|
| Billing calculation core | May return in-memory invoice/credit-note projections but must not call EF persistence or the scheduler. |
| Persistence boundary | Only the real persist boundary may add invoice/credit note, finance log, scheduler, and SaveChangesAsync. |
| Preview Billing Diff | Read-only; no invoice, scheduler, finance log, or projection writes. |
| Payment domain | Reads invoice/payment option to collect money, but financial document state still belongs to Finance. |
| Accounting sync | Provider sync is a side effect; AIMY Finance records remain the internal business source. |
Invoice email flow
- Single vs batch — a batch of one site uses that site's template; multiple sites → a separate editor per site, the payload only contains that site's invoices (INV-INVOICE-EMAIL-002).
- Provider-independent — both Xero and AIMY Accounting allow invoice email (INV-INVOICE-EMAIL-001).
- Placeholder render —
,,resolve at generate time; unresolved placeholders do not exist in the sent email (INV-INVOICE-EMAIL-005).
Real-world example: Admin selects 5 invoices from 2 sites then Email → the composer shows 2 editors per site → sends each payload to its site; the customer gets a link that requires login.
Invoice reminder scheduling flow
- Daily evaluator — runs on the BU local calendar; computes the target
Invoice.Date/DueDate ∓ N; no backfill for days before the rule was enabled (INV-INVOICE-REMINDER-002). - Eligibility re-check — right before creating the notification: active,
AmountDue > 0, not in DD collection flow,LockTypeIdnull; each exclusion records a structured skip reason, does not setInvoice.Sent(INV-INVOICE-REMINDER-003). - Idempotent send — claims by
(Invoice.Guid, rule ID)with a uniqueness guard; a duplicate worker → only one creates the logical send (INV-INVOICE-REMINDER-005). - History — 1 entry on Invoice Transaction + Account, correlated with OutgoingMessage; retries do not create duplicate entries (INV-INVOICE-REMINDER-006).
Real-world example: Rule
BeforeDueDate(7), invoice due 20/07 → on 13/07 (BU time) the evaluator selects the still-owing invoice, not in DD/lock → creates 1 reminder send + 1 history entry; a duplicate worker does not send a second time.
FinanceTransaction projection sync through worker
Booking numbers and credit-note allocation sync through finance-transaction-queue, not SQL triggers.
Sync_Invoice_BookingNumbers— enqueued whenTermBookingInvoiceLoginserts/updates/deactivates; the worker computesBookingNumbersasBK-{id}(status 150/200/250) orREQ-{id}, rebuildsSearchKeyby the formulaNaN; idempotent, the old trigger is dropped after the worker is verified.- Credit-note allocation —
HandleFinanceTransactionhandlesAllocate_CreditNote/Deallocate_CreditNote: loads CreditNote fresh from DB, updatesAmountDue/AmountCredited/AmountPaid/StatusId; CreditNote not found → logs error; no FT row → skip, does not create one. - Projection refresh —
ActionTypeId = Refresh_FinanceTransaction_Projection,EntityGuid = FinanceTransaction.Guid, RowKeyfinance-transaction-projection:{Guid}; dedup before dispatch viaTux.Workers.EventScheduler; latest source state wins.
Real-world example: Confirming a booking creates a new
TermBookingInvoiceLog→ enqueuesSync_Invoice_BookingNumbers→ worker writesBookingNumbers = "BK-1234", rebuilds SearchKey; Invoice Manager shows the Booking ID matching the active association.
Common data
| Group | Examples |
|---|---|
| Billing | Billing, billing period, payment option, split rule, currency setting. |
| Invoice | Invoice, InvoiceLine, invoice scheduler, invoice lock/processing state. |
| Credit note | CreditNote, credit-note line, allocation/link to invoice. |
| Projection | Finance transaction search key, booking number sync, projection refresh state. |
| Email/reminder | Notification template, Invoice_Fully_Paid event/hint, reminder rule, Viewed/Sent flag. |
| Settings | Finance/accounting settings, invoice templates, Xero provider config. |
| Subsidy licence | Subsidy licence (SWN/Program ID) bound to the site via Licence.OrgId. |
Drift to keep in mind
- The projection may need a refresh when booking numbers, credit-note allocation, or invoice status change.
- Invoice lock/processing prevents admin operations from touching documents a job is currently processing.
- Preview Billing Diff uses the same calculation logic but must keep zero side effect.
- Empty
BookingNumbersin the projection does not prove the invoice has no booking — cross-check the source association first (invoice-manager-booking-id-integrity).