Finance — Core concepts
Core objects
| Concept | Meaning |
|---|---|
| Billing | Aggregate that groups charge obligations from booking/subscription; owns the payment option, split context, and data to generate invoices. |
| BillingPeriod | Financial period or stage used to group lines, especially the subscription billing schedule. |
| Invoice | Receivable document sent to the customer/account; has header, lines, tax/discount, status, and a scheduler that processes it. |
| InvoiceLine | Specific fee line: session, add-on, discount, adjustment, or booking-related metadata. |
| CreditNote | Document that reverses an invoice when money is reduced, cancelled, or the booking is edited after the invoice has been issued/paid. |
| FinanceTransaction | Projection for current-state queries on invoice/credit-note/payment; keeps list/search fast and stable. |
| InvoiceScheduler | Job/scheduler that issues or processes invoices at the right time. |
| Preview Billing Difference | Read-only computation for booking edits: computes the expected invoice/credit-note without persisting. |
| Accounting Provider | Accounting provider such as Xero; receives invoice/credit-note/payment sync from AIMY. |
Billing vs Invoice
Billing is the charging context; Invoice is the document the customer sees, finance operates on. One billing can create multiple invoices when the split rule splits by org, attendee, category, or period.
Invoice vs FinanceTransaction
Invoice is the original business record. FinanceTransaction is a projection/read model for search, list, booking number sync, allocation, and aggregate state. Drift between the two → the source of truth is still the original record; the projection needs refresh or reconcile.
Subscription Enrichment: keep the two charge paths separate
Subscription has two kinds of money that must remain separate:
| Source | Finance handling | Do not do |
|---|---|---|
| Base schedule + Add-on | SubscriptionBillingService reads EnrollmentBillingSchedule and creates the Subscription invoice. | Do not read the amount from the Enrichment TermBookingLine. |
| Enrichment occurrence | Booking Billing/Reconciliation reads the priced TermBookingLine and creates Booking invoice/credit logs. | Do not write the amount back into the Subscription schedule or Subscription invoice. |
Each Enrichment occurrence must have one authoritative Booking line identified by TermBookingId + TermProductId + Date; the same occurrence must not be charged through both paths. Subscription InvoiceLine may still contain a valid base price line, including a zero unit price, but it must not reference an Enrichment Booking line.
Real-world example: The Subscription schedule is $5 and the Booking has five Enrichment occurrences at $25. → The Subscription invoice is $5; Booking reconciliation processes $25. → The two documents respect the boundary instead of becoming one $30 Subscription invoice.
Credit note
A credit note appears when edit/cancel reduces the financial obligation after the invoice has been issued or paid. It is not "deleting the invoice"; it is an adjustment document that keeps the financial history auditable.
Billing generation — how documents are generated
Billing holds the charging obligation; billing generation is the pipeline that turns a confirmed booking/enrollment into documents (Invoice, CreditNote). It separates 3 layers (invariant INV-BILLING-CORE-001):
| Layer | What it does |
|---|---|
| Prepare | Gathers booking/order/line/extra/discount/split into a standard input. |
| Calculation core | Computes invoice, line, tax, discount, split group, total, credit-note impact — in memory only, no persistence. |
| Persistence | Saves invoice/credit note, finance log, scheduler, SaveChangesAsync. |
Preview Billing Diff and the real confirm share this same core → the projected output matches the real output for eligible/parity-safe states. States that cannot guarantee parity return a structured error with no indicative amount. Backbone rules (each rule includes an example):
- Confirm gate — confirm only finalizes after billing regeneration succeeds; billing fail → confirm fail, no side effect recorded (INV-CONFIRM-BILLING-001).
Real-world example: A parent confirms a Pay Now booking but the DD config is broken → the booking does not go to
Approved, returns failure → admin fixes the config and confirms again. - Frequency override — Pay Now →
One_Off, Direct Debit →WeeklyviaResolveBillingFrequencyOverride; the real path and preview share this helper.Real-world example: A customer chooses Direct Debit at checkout → the invoice splits weekly so DD collects the right period.
- Net difference —
Difference = new invoices − new credit notes − scheduled-delete invoices; the backend is authoritative.Real-world example: A new $55 invoice replaces an old $65 invoice → Preview subtracts the scheduled deletion and shows the net impact.
- Billing scope & attendee breakdown — Preview calculates once across the whole billing scope, then allocates the result to submitted attendees; another booking group sharing the same
BillingIdgoes intoOther booking groups, and unclear attribution goes intoShared / Unallocated. Rows must reconcile toSummary, not independent calculations.Real-world example: One BillingId has a one-attendee group and a two-attendee group, but only the two-attendee group is edited → Preview separates the 2 edited attendees, groups the other group, while the total net impact still covers the whole billing record.
- Whole-scope net impact —
Summary.Difference/Net billing impactisnew invoices − credit notes − scheduled-delete invoices; it is the value used for Confirm, not the booking form'sEstimated fee per attendee.Real-world example: The form shows
$65/attendee while Preview shows+$68→ the two values have different scope/meaning; do not divide+$68by the attendee count to infer each person's price. - Unsupported or ambiguous state — mixed retained + Generated invoices can still be previewed when retained logs are consistent and the context can be reconstructed; incomplete context, unmappable retained logs, or invalid scope must return a structured error and show no indicative amount.
Real-world example: Billing has retained and Generated invoices with clearly mapped line logs → Preview continues and schedules the unpaid Generated invoice for deletion. If retained logs leave an unexplained residual → block instead of guessing the amount.
- Deposit — confirming with a deposit still generates an invoice for the full amount; settlement sits outside billing generation (INV-DEP-015).
Real-world example: A customer puts down a 20% deposit → the invoice records the full amount; the deposit is handled by Payment, the invoice is not changed.
- Approve / Skip subscription — approve generates an invoice via
UpdateBillingByIdAsync; skip with an invoice already Paid/Sent/Synced issues a credit note, otherwise regenerates.Real-world example: Admin approves the May period → creates TermBooking + invoice + Approved log; skipping an already-charged period → a credit note reverses it.
- Price line mandatory — subscription generation needs
SubscriptionPriceId+ ≥1 active price line; missing → fail before persist, no fallback from the schedule amount (INV-SUB-BM-04).Real-world example: A Weekly plan without price lines → generate fails with structured validation; after adding them the invoice appears, an empty invoice is never generated.
Invoice email
Invoice email is the channel that sends invoices to customers through a Notification template, available regardless of accounting provider (Xero or AIMY Accounting) (INV-INVOICE-EMAIL-001).
- Composer — opens before scheduling; shows invoice count, recipient, From, Email Reply To, Subject, Body; Subject/Body editable before Send (INV-INVOICE-EMAIL-002).
- Single & batch — batch across sites → each site has its own editor based on the site's template + Email Reply To; cancel/close composer → nothing scheduled.
- No PDF attachment — the email does not attach the invoice PDF; there is no "Include Invoice PDF" control, legacy payloads cannot restore the attachment (INV-INVOICE-EMAIL-003).
- Placeholders —
,,; links HTTPS, use opaque guid/token, do not expose numeric id (INV-INVOICE-EMAIL-005). - Customer link — View/Download require authenticate + authorize by account/BU; unauthorized returns not-found/forbidden, no metadata leak (INV-INVOICE-LINK-001).
Real-world example: Admin opens invoice AIN-0042 Approved in BU Xero, clicks Email → the composer shows the site template, admin edits Subject/Body then Send → the email goes out without PDF; the customer must log in to open the View/Download link.
Invoice reminders
Reminders are automatic debt notices sent through the same Invoice Email pipeline, configured per business unit.
| Rule | Content |
|---|---|
| Limit | ≤3 active rules/BU; add/edit/delete; disable keeps the rule, blocks new runs |
| Rule types | OnInvoiceDate (no offset), BeforeDueDate(N), AfterDueDate(N) with offset 0–365 |
| Duplicate target | 2 rules cannot share an effective target (including 0 days before/0 days after) |
| Target date | Invoice.Date; DueDate ∓ N by BU local calendar/timezone; no backfill before enable date |
| Eligibility | active, same BU, AmountDue > 0, not in DD collection flow, LockTypeId null |
| Template | rendered from the site's Invoice Email template at send time; no separate sender/subject/attachment stored |
| Idempotent | 1 logical send per (invoice, rule); Invoice.Sent only true after the NotificationJob is created |
Real-world example: Admin creates rule
AfterDueDate(7); invoice AIN-0031 still owes, not in the DD collection flow → the evaluator runs on due+7 by BU time → sends a reminder with the current Invoice Email template, records 1 history entry on Account + Invoice Transaction.
Invoice fully-paid notification
Invoice_Fully_Paid is a specialized EntityEvent published when an invoice moves to FinanceStatus.Paid (INV-INVOICE-PAID-NOTIF-001).
- Trigger — every path that sets
Invoice.StatusId = Paid(payment or credit-note allocation makingAmountDue = 0) publishes the event after commit, only on a real transition. - Exceptions — an invoice with
TotalAmount = 0marked Paid because nothing is collected → not published; an invoice imported/synced already Paid → not published. - Hint — the event carries
NotificationEventTypeId = PaymentSuccessConfirmation(810); the worker creates the NotificationJob via the hint path, not by callingCreateNotificationJobAsyncdirectly (INV-INVOICE-PAID-NOTIF-002). - Template — default from NotificationOption, site override via NotificationSetting by
OrgId; email disabled at the site → not sent (INV-INVOICE-PAID-NOTIF-003). - Recipient — primary contact of the account; missing email → job completes without delivery, no throw (INV-INVOICE-PAID-NOTIF-004).
Real-world example: A parent pays the remaining balance of invoice AIN-0027 →
AmountDue = 0→ the transaction commits then publishesInvoice_Fully_Paid→ worker creates a PaymentSuccessConfirmation job; the confirmation email goes to the primary contact if the site enables the email channel.
Invoice viewed tracking
Invoice.Viewed (boolean) stores whether the customer has successfully accessed the invoice (INV-INVOICE-VIEW-001).
- Default
false; only settruewhen the customer is authenticated + authorized and View/Download PDF succeeds (INV-INVOICE-VIEW-002). - Monotonic — not reset by resend email, edit, paid, or sync; repeated access is idempotent.
- Not set — admin opening Invoice Details, email schedule/delivery, unauthorized request, or PDF failure.
- Display — Invoice Details API returns
Viewed; Booking Manager only shows theViewedtag when the state is true (INV-INVOICE-VIEW-003).
Real-world example: Customer opens the View Invoice link AIN-0012 for the first time → the system sets
Viewed = true→ admin sees theViewedtag; resending email does not remove the tag.
Invoice lock & tag "Processing"
An invoice in a payment processing flow has LockTypeId != null and is marked "processing" in Invoice Manager.
- List — shows a
Processingtag below the invoice number whenLockTypeId != null; does not replace or mutate status (INV-INV-MGR-LOCK-001). - Hover — tooltip shows the exact content
Payment in processing(INV-INV-MGR-LOCK-002). - Details — title shows
Payment in processingafter Invoice Status;Add PaymentandApply Creditvisible but disabled (INV-INV-MGR-LOCK-003/005). - Display-only — the tag creates no new invoice status and does not change the payment/lock lifecycle (INV-INV-MGR-LOCK-004).
Real-world example: A payment is processing, invoice AIN-0090 has
LockTypeId != null→ the list shows the Processing tag below the invoice number; details showAdd Payment/Apply Creditdisabled while the invoice status stays unchanged.
Ownership summary
| Owner | Owns |
|---|---|
| Billing | Payment option on billing, split context, data prepared for invoice generation. |
| Invoice | Header/line/status/lock of the invoice. |
| CreditNote | Header/line/allocation of the credit note. |
| FinanceTransaction | Projection/search state, can refresh from the source. |
| Payment | Collects money and provider transaction; does not own the invoice lifecycle. |
Closure Day credit and balance audit
- The credit basis is
TermBookingInvoiceLog.AmountonSubmitted/Approved/Paidinvoices minus active credit-note logs; it does not useTermBookingLine.TotalAmount. - The credit note preserves source tax rate, currency and line-amount type. Mixed treatments split into the correct component/header; ambiguous historical mapping fails before mutation. Converted lines clear amount/discount/promo and cannot return through billing regeneration.
GET api/ClosureDay/balance-audit?closureDayIds=...is read-only, limited to 50 closure days per request, scoped to BU andSETUP_CLOSURE_DAY; it returns booking total, active line total and invoiced-minus-credited.- Cancel-and-credit uses a serializable transaction; the credit note and zero line commit before the Xero event.
Real-world example: A closure-day line is $100, $60 was invoiced and $10 credited. → Credit another $50; the audit shows $60 − $10, not a $100 credit.
Invoice grouping and Booking Info
Subscription invoices may group attendees by split rule while InvoiceLine retains attendee ownership. In Invoice Manager, a Booking ID with billing opens read-only Booking Info, loads every active booking for the billing, checks BU/customer scope and does not edit staff notes.
Real-world example: An invoice covers bookings from several programmes. → Open the Billing → programme → booking hierarchy through Booking Info, then go to Booking Manager with the billing filter.