Finance module
The Finance module covers invoicing, payment collection, expenses, and revenue reporting. It integrates with Stripe for customer-facing payment links and emits webhook events on every state change so external systems stay in sync.
Entities
| Entity | What it represents |
|---|---|
| Invoice | Issued bill to a customer. Has invoice_number, status (draft/sent/overdue/paid), due_date, line items, currency, total. |
| Recurring invoice | A template that the daily cron clones into new invoices on a frequency (weekly/biweekly/monthly/quarterly/annually). |
| Payment | A received payment. Has amount, method, reference, optional stripe_session_id linking back to a Checkout Session. |
| Expense | An outgoing cost. Has vendor, category, amount, receipt URL, billable flag. |
| Payment link | A Stripe Checkout Session for an invoice; can be regenerated, expires after 24h. |
Schema lives under the finance schema with backward-compatible
views in public (e.g. public.invoices → finance.invoices).
Invoice lifecycle
Invoices move through five states:
draft → sent → overdue → paid
↘ paid
State transitions emit finance.invoice.* events. The most
useful ones for integrations:
| Event | Fires when |
|---|---|
finance.invoice.sent | Invoice emailed to the customer. |
finance.invoice.paid | Stripe webhook confirms payment OR a payment is manually recorded that brings the total to >= invoice.total. |
finance.invoice.overdue | Daily reminder cron flips sent → overdue for invoices past due. |
Payment links (Stripe)
Customer-facing payment via Stripe Checkout. The flow:
- Operator clicks Payment link on an invoice in the dashboard.
- We create a Stripe Checkout Session in
paymentmode tagged withccd_invoice=true+ the tenant + invoice id. - Operator copies the URL and shares with the customer.
- Customer pays; Stripe fires
checkout.session.completed. - Our webhook handler writes a
paymentsrow keyed on the session id (UNIQUE — duplicate delivery is a no-op) and marks the invoice paid + emitsfinance.invoice.paid.
Sessions are idempotent: a second click on Payment link while the previous session is still open returns the existing URL.
Recurring invoices
Define a recurring template, then the daily recurring-invoices
cron generates fresh invoices on schedule. Each generation:
- Clones the template's line items + customer + currency.
- Stamps a unique invoice number
INV-YYYYMM-NNNN. - Sets
issue_date=today,due_date=today+30. - If
auto_send=true, emails the customer + flips status tosent(in a single atomic step that's safe to retry).
Deactivates when end_date passes.
Invoice reminders
Daily cron walks invoices status IN ('sent','overdue') with
due_date < today and emails a polite nudge to the contact. A
per-invoice 7-day cooldown prevents nagging. Flips sent → overdue
on the first reminder.
Expense tracking
Lightweight: expense rows have a vendor, category, amount, and optional billable flag. Billable expenses flagged against a project surface as draft invoice line items the same way billable time entries do.
Receipt upload goes to the content-assets storage bucket scoped
to the tenant; the file URL is stored on the row.
Cross-module insights
- Finance × CRM: deal + linked-contact's invoice history on the contact detail page.
- Finance × Projects: billable time → draft invoice generator.
- Finance × Analytics: scheduled reports for revenue by month, invoice aging, and pipeline-to-revenue attribution.
Permissions
finance.invoices.read|create|update|delete,
finance.payments.read|create|update|delete, etc. Operator-only
endpoints (payment-link creation, recurring template edits) gate
on finance.invoices.update.
Read next
- Finance API — invoice/payment-link endpoints + Stripe webhook reference.
- Analytics module — where scheduled revenue reports live.