Skip to content

SlimFact Invoice Lifecycle

A complete reference for how bills, receipts, invoices, payments, refunds, reminders, exhortations, and subscriptions work in SlimFact.


Statuses

InvoiceStatus

StatusMeaning
CONCEPTDraft — still being edited, not yet sent
OPENSent to customer, awaiting payment (immutable)
PAIDFully paid
BILLA payment request — not an invoice yet
RECEIPTProof of payment — created from a paid bill
CANCELEDVoid

PaymentStatus (checkout.payments)

OPEN, CANCELED, PENDING, AUTHORIZED, EXPIRED, FAILED, PAID

RefundStatus (checkout.refunds)

QUEUED, PENDING, PROCESSING, REFUNDED, FAILED, CANCELED


Invoice Lifecycle

                               ┌──────────┐
                               │ CANCELED │ ← terminal
                               └──────────┘
                                    ^
                                    │ cancelInvoice (any status)
    ┌───────────────────────────────┼───────────────────┐
    │                               │                   │
    v                               │                   v
┌─────────┐                         │              ┌─────────┐
│ CONCEPT │─────────────────────────┤              │  BILL   │
│ (draft) │                         │              │         │
└─────────┘                         │              └─────────┘
    │                               │                   │
    │ sendInvoice                   │                   │ sendReceipt
    │ (assigns number, date,        │                   │ (amountDue must be 0)
    │  due date)                    │                   │
    │                               │                   v
    v                               │              ┌─────────┐
┌─────────┐                         │              │ RECEIPT │ ← terminal
│  OPEN   │─────────────────────────┤              │ (proof) │    for bills
│ (sent)  │                         │              └─────────┘
└─────────┘                         │
    │                               │
    │ payment covers                │
    │ amountDue                     │
    v                               │
┌─────────┐                     (can cancel
│  PAID   │                      any status)
└─────────┘

Valid Transitions (setInvoiceStatus)

FromToCondition
CONCEPTOPENDirect — sendInvoice assigns number, date, due date
CONCEPTCANCELEDDirect
BILLRECEIPTamountDue must be 0 (fully paid). Only from BILL
BILLCANCELEDDirect (unpaid bills only)
OPENPAIDamountDue must be 0
RECEIPTPAIDConvert receipt to invoice

Blocked transitions:

AttemptReason
Any → CONCEPT"Cannot convert an invoice back to concept"
Any → RECEIPT (except BILL)"Can only create receipts for bills"
Any → RECEIPT when amountDue > 0"Cannot create a receipt when amount due is not zero"
Any → PAID when amountDue > 0"Cannot set paid status when amount due is not zero"
BILL → OPEN"Can only open concept invoices"
RECEIPT → OPEN"Can only open concept invoices"
OPEN/PAID/RECEIPT → CANCELED"Only concepts and unpaid bills can be canceled"

Document Types & Semantics

Invoice

  • Starts as CONCEPT (draft)
  • sendInvoiceOPEN: assigns invoice number, date, and due date. Becomes immutable.
  • Payment received → PAID
  • Can be canceled at any stage

Bill

  • Created with status BILL
  • Behaves like an invoice but represents a payment request
  • Must be paid first before it can become a receipt
  • sendReceiptRECEIPT (requires amountDue === 0)
  • Subscription cron jobs can create bills by setting type: 'bill'

Receipt

  • Created from a paid bill via sendReceipt
  • Proof of payment document
  • Can be converted to an invoice via sendInvoice (status → OPEN)
  • PDF rendering shows tax-inclusive amounts (like bills)

Numbering

Invoice numbers are assigned when status transitions to OPEN (via sendInvoice). The number is assigned atomically using a serializable transaction:

  1. Read initialNumberForPrefix by numberPrefixTemplate
  2. Count existing OPEN invoices with same prefix
  3. Compute next number: initialNumber + count
  4. Fill in {num}, {year}, {month} placeholders in the template

CONCEPT and BILL invoices have no number yet. CONCEPT and BILL invoices have no number yet. Receipts converted to invoices get PAID status (already numbered).


Payment Flow

Adding a Payment

  1. Admin or customer calls addPaymentToInvoice
  2. Payment method routing:
    • idealIDEAL_PAYMENT_HANDLER env var (default: mollie)
    • creditcardCREDITCARD_PAYMENT_HANDLER env var (default: stripe)
    • cash → always available
    • bankTransfer → always available
    • pin → available if pinEnabled
  3. PSP handler's createPayment creates an external payment record and a local checkout.payments row
  4. If online payment (iDEAL/creditcard): returns checkoutUrl for redirect
  5. If offline (cash/bank transfer): records payment immediately

Webhook Settlement

Mollie (POST /mollie/webhook):

  1. Receives { id: molliePaymentId }
  2. Looks up local payment by externalId
  3. Calls mollieHandler.settlePayment({ externalId }) → syncs status from Mollie
  4. If amountPaid >= totalIncludingTax and status is OPENsetInvoiceStatus(PAID)
  5. Calls fetchWebhookUrl(invoice) if external webhook URL configured

Stripe (POST /stripe/webhook):

  1. Verifies webhook signature using STRIPE_WEBHOOK_SECRET
  2. Filters for checkout.session.completed or payment_intent.succeeded
  3. Idempotency guard: checks if invoice was already OPEN before transitioning
  4. Calls stripeHandler.settlePayment({ externalId }) → syncs from Stripe
  5. If wasOpen === true and fully paid → setInvoiceStatus(PAID)
  6. Calls fetchWebhookUrl(invoice)

Refunds

  1. Admin calls refundInvoice({ id })
  2. Finds the highest paid payment from a supported PSP where amountDue < 0
  3. Calls PSP's refundPayment({ id, amount: -amountDue })
  4. PSP creates external refund, inserts into checkout.refunds
  5. A daily cron job (checkRefunds, runs at midnight) syncs pending refund statuses

Reminders & Exhortations

Flow

Invoice due date passes

    ├─ 1st reminder (send immediately after due)
    │       │
    │       └─ Must wait 7 days after last reminder

    ├─ 2nd reminder (7 days after 1st)
    │       │
    │       └─ Must wait 7 days after last reminder

    └─ Exhortation (7 days after 2nd)

            └─ Requires at least 2 reminders sent

Rules

  • Max 2 reminders — after 2, you must send an exhortation instead
  • 7-day waiting period between each reminder/exhortation
  • Invoice must be past its due date
  • Email templates for remindInvoice and exhortInvoice are customizable per locale
  • reminderSentDates is tracked on the invoice

Subscriptions

Creating a Subscription

  1. Admin creates a subscription with:
    • companyId, clientId, numberPrefixTemplate
    • cronSchedule — standard cron expression
    • startDate, optional endDate
    • type'invoice' or 'bill'
    • lines[] — same as invoice lines

How It Runs

  1. startSubscription is called when a subscription is created or activated
  2. Fetches company/client details from DB
  3. Builds a RawNewInvoice:
    • If type === 'bill': status = InvoiceStatus.BILL
    • If type === 'invoice': status = undefined (defaults to CONCEPT)
  4. Creates a pg-boss job queue named subscription.{uuid}
  5. Schedules the cron with the subscription's cron expression
  6. Each cron tick: invoiceHandler.createInvoice(job.data) creates a new invoice
  7. The new invoice gets its own number when sent (via sendInvoice)

Managing Subscriptions

  • Active/inactive toggle — calls stopSubscription or startSubscription
  • Edit — updates subscription and restarts the cron job
  • Delete — stops the cron job and removes the record

Key Environment Variables

VariablePurpose
MOLLIE_API_KEYMollie API key (test/live)
MOLLIE_API_KEY_<PREFIX>Company-specific Mollie key (multi-company)
STRIPE_API_KEYStripe secret key
STRIPE_API_KEY_<PREFIX>Company-specific Stripe key (multi-company)
STRIPE_WEBHOOK_SECRETStripe webhook signing secret for signature verification
IDEAL_PAYMENT_HANDLERmollie or stripe (default: mollie)
CREDITCARD_PAYMENT_HANDLERmollie or stripe (default: stripe)
MODULARAPI_ADMIN_PASSWORDPassword for the seed-created admin account

Email System

Event Types

EventTemplate NameTrigger
sendInvoicesendInvoiceAdmin/public sends an invoice
sendReceiptsendReceiptAdmin converts bill to receipt
remindInvoiceremindInvoiceAdmin sends a payment reminder
exhortInvoiceexhortInvoiceAdmin sends a formal exhortation
replyInvoicereplyInvoiceAdmin replies to invoice thread

Features

  • Handlebars templates, , , etc.
  • Per-locale variantsen-US, nl-NL, de-DE
  • Open tracking?eventType=emailOpened query parameter in the invoice link
  • Per-company settings — custom MAIL_FROM, EMAIL_BCC per company
  • PDF attachment — invoice PDF attached automatically on every send/remind/exhort
  • UBL XML attachment — attached automatically when sending OPEN/PAID invoices

Copyright © simsustech 2023-present