Payments and billing

Billing is built in: a catalogue, charges, invoices, and payments online, by bank transfer or in cash. It fits anyone who bills people, from a school canteen to club memberships.

The pieces

IssuerThe billing entity, e.g. a parents’ committee or an after-school club. Each has its own bank account, invoice numbering and payment provider
ItemA catalogue entry with a price and a unit: each, day, month or year
AccountWho a charge is for, e.g. a student. Payers are resolved from the app’s own data (custodies by default), so billing doesn’t keep a separate list
ChargeOne item billed to one account: posted manually (optionally for a whole group) or generated from a subscription
SubscriptionA recurring charge: meals on Mondays and Thursdays, a monthly fee, a year of chess club
InvoiceAn account’s open charges, with a number, a due date, and a structured reference used to match bank transfers
PaymentSettles invoices: transfer, cash, or online
WalletPrepaid balance. A booked meal is debited immediately and refunded if cancelled in time

All amounts are integer cents, never floats.

Billing a period

app.ts
const billing = sluurp.billing;

await billing.charge({ account: pupil.id, item: trip.id });   // one charge
await billing.run({ dry: true });                             // preview a run
await billing.run({});                                        // bill subscriptions, invoice open charges

remind(id) emails a reminder for an unpaid invoice; cancel(id) cancels it.

Collecting payments

  • Online via Stripe or Mollie (per issuer); both support cards and Bancontact. billing.pay({ invoice, back }) returns a checkout url; the user returns to back with ?paid=<id>, and the provider’s webhook records the payment.
  • Bank transfer: pass bank statement lines to billing.match(lines); each is matched to its invoice by reference. Use dry to preview.
  • Cash: billing.record(payment).
  • Wallet top-up: billing.pay({ account, issuer, amount_cents }), between €1 and €1,000.

Permissions

Managing billing (catalogue, charges, runs, recording payments) is governed by the manage rule in billing settings; without one, only superusers can. Everyone else sees their own accounts and those they pay for. billing.certificate(account, year) returns the tax-deductible amount paid per issuer for a year (in Belgium: childcare for children under 14).

Stripe, Mollie and Paddle accounts are configured under Payments in the admin UI.

Paddle is a merchant of record: it charges the sales tax and pays it where it is due. Its checkout opens on a page the server serves, /api/payments/paddle/<payee>, so a Paddle payee also needs Paddle’s client-side token, and the site approved in Paddle’s checkout settings. A pdl_sdbx_… key pays in Paddle’s sandbox.