Invoicing — Invoices, Credit Notes & Partial Payments
Granit.Invoicing manages the full invoice lifecycle from draft to paid, including
credit notes, partial payments, and overpayment detection. It is agnostic — any
module can create invoices via CreateInvoiceCommand, not just Subscriptions.
Package structure
Section titled “Package structure”DirectoryGranit.Invoicing/ Domain: Invoice (unified with CreditNote), line items, tax, provider abstractions
- Granit.Invoicing.EntityFrameworkCore EF Core store (SQL Server / PostgreSQL)
- Granit.Invoicing.Endpoints Minimal API (invoice listing, PDF download) with RBAC
- Granit.Invoicing CreateInvoiceHandler, payment event handlers
- Granit.Invoicing.BackgroundJobs Overdue invoice scan
- Granit.Invoicing.Internal Self-hosted PDF via Granit.Templating (Phase 2)
- Granit.Invoicing.Odoo External accounting sync via XML-RPC (Phase 2)
- Granit.Invoicing.EuVat EU VAT calculator with VIES validation (Phase 2)
Invoice lifecycle
Section titled “Invoice lifecycle”stateDiagram-v2
[*] --> Draft: Create()
Draft --> Open: Finalize()
Open --> Paid: RecordPayment() / ApplyCreditNote()
Open --> Void: VoidInvoice()
Open --> Uncollectible: MarkUncollectible()
Uncollectible --> Paid: RecordPayment() (recovery)
Uncollectible --> Void: VoidInvoice()
- Draft: mutable — add line items, set billing address, adjust tax
- Open: finalized, immutable financial fields, awaiting payment
- Paid: payment received (terminal)
- Void: cancelled with paper trail (terminal)
- Uncollectible: bad debt write-off, can still recover
Unified Invoice + Credit Note
Section titled “Unified Invoice + Credit Note”Invoices and credit notes share the same aggregate, differentiated by
InvoiceDocumentType. Value Objects group related parameters:
BillingPeriod(Start, End)— billing period datesCreditNoteInfo(ParentInvoiceId, Reason)— credit note link + reasonLineItemSource(Type, Id?)— agnostic source reference
// Regular invoice with billing periodvar invoice = Invoice.Create(id, tenantId, InvoiceDocumentType.Invoice, "EUR", CollectionMethod.Auto, BillingReason.SubscriptionCycle, period: new BillingPeriod(periodStart, periodEnd));
// Credit note (linked to parent invoice)var creditNote = Invoice.CreateCreditNote(id, tenantId, parentInvoiceId, "EUR", "Service interruption compensation");When a credit note is finalized, CreditNoteIssuedEto is published instead
of InvoiceFinalizedEto.
Partial payments
Section titled “Partial payments”RecordPayment() accumulates payments rather than replacing the amount.
Auto-transition to Paid occurs when AmountRemaining <= tolerance:
invoice.RecordPayment(10.00m, paidAt); // AmountPaid = 10, still Openinvoice.RecordPayment(19.99m, paidAt); // AmountPaid = 29.99, → Paid
// With tolerance (SEPA rounding)invoice.RecordPayment(29.98m, paidAt, tolerance: 0.05m); // → PaidCredit note application
Section titled “Credit note application”ApplyCreditNote() enables Paid transition without any payment — critical
for the event choreography (Subscriptions renews on InvoicePaidEto, not on
PaymentSucceededEto):
invoice.ApplyCreditNote(29.99m, issuedAt); // AmountCredited = 29.99, → PaidOverpayment detection
Section titled “Overpayment detection”When AmountPaid + AmountCredited > Total, the Overpayment property tracks
the excess and OverpaymentDetectedEto is published for reconciliation.
Line item source types
Section titled “Line item source types”public enum InvoiceSourceType{ Subscription, // Fixed plan charges Usage, // Metered usage charges OneShot, // One-time purchases Credit, // Credit adjustments}This makes Invoicing agnostic — future Granit.Commerce can create invoices
with OneShot line items using the exact same infrastructure.
Provider abstractions
Section titled “Provider abstractions”| Interface | Purpose | Phase |
|---|---|---|
ITaxCalculator | Tax computation per line item | 1 (stub) |
IInvoiceSyncProvider | External accounting sync (Odoo, Sage) | 2 |
IInvoiceDocumentGenerator | PDF generation via Granit.Templating | 2 |
IInvoiceNumberGenerator | Gap-free sequential numbering with InvoiceDocumentType | 1 (interface) |
Decoupled command
Section titled “Decoupled command”Any module can create an invoice without referencing Invoicing directly:
var command = new CreateInvoiceCommand( TenantId: tenantId, Currency: "EUR", CollectionMethod: CollectionMethod.Auto, BillingReason: BillingReason.SubscriptionCycle, LineItems: [ new("Pro Plan — Monthly", 1, 29.99m, InvoiceSourceType.Subscription), new("API Calls: 15,000 requests", 15000, 0.001m, InvoiceSourceType.Usage), ], PeriodStart: periodStart, PeriodEnd: periodEnd);
await messageBus.SendAsync(command);Idempotency key
Section titled “Idempotency key”CreateInvoiceCommand accepts an optional IdempotencyKey to prevent
duplicate invoice creation on handler retries:
new CreateInvoiceCommand( ..., IdempotencyKey: $"billing-cycle-{subscriptionId}-{periodEnd:yyyyMMdd}");Key derivation by handler:
| Handler | Key format |
|---|---|
BillingCycleCompletedHandler | billing-cycle-{subscriptionId}-{periodEnd:yyyyMMdd} |
UsageSummaryReadyHandler | usage-{tenantId}-{meterId}-{periodEnd:yyyyMMdd} |
Domain services
Section titled “Domain services”Business logic is encapsulated in domain services, keeping handlers as thin pass-through delegates.
| Interface | Responsibility | Package |
|---|---|---|
IInvoiceCreationService | Creates draft invoices, calculates tax, generates numbers, finalizes | Granit.Invoicing |
IOverdueInvoiceDetectionService | Scans open invoices past due date, publishes InvoiceOverdueEto | Granit.Invoicing |
IInvoiceCreationService is the domain entry point for CreateInvoiceCommand
processing. It handles the full draft → finalize flow including optional
ITaxCalculator and IInvoiceNumberGenerator integration:
public interface IInvoiceCreationService{ Task CreateAsync(CreateInvoiceCommand command, CancellationToken cancellationToken = default);}Both services are registered automatically by AddGranitInvoicing().
Admin API
Section titled “Admin API”| Method | Route | Permission |
|---|---|---|
| GET | /api/granit/invoicing/invoices | Invoicing.Invoices.Read |
| GET | /api/granit/invoicing/invoices/{id} | Invoicing.Invoices.Read |
| GET | /api/granit/invoicing/invoices/{id}/pdf | Invoicing.Invoices.Download |
| POST | /api/granit/invoicing/credit-notes | Invoicing.CreditNotes.Manage |
Notifications
Section titled “Notifications”Granit.Invoicing.Notifications defines 4 notification types:
| Type | Name | Severity | Opt-out |
|---|---|---|---|
InvoiceIssuedNotificationType | Invoicing.InvoiceIssued | Info | Yes |
InvoiceOverdueNotificationType | Invoicing.InvoiceOverdue | Warning | No |
InvoicePaidNotificationType | Invoicing.InvoicePaid | Success | Yes |
CreditNoteIssuedNotificationType | Invoicing.CreditNoteIssued | Info | Yes |
See also
Section titled “See also”- SaaS Overview — ecosystem architecture and choreography
- Subscriptions — billing cycle orchestration
- Payments — payment collection and webhooks