Rolling out invoicing and online payments
This release makes the customer invoice the single document for money a customer owes, lets customers pay from a hosted page, and tightens how online payments are confirmed. Work through the steps in order.
1. Run the migrations
Section titled “1. Run the migrations”| Database | Migration | What it adds |
|---|---|---|
| Global | 0036_payment_intents |
A platform-wide index of every online payment attempt, so a gateway notification can be matched to the right tenant; tracked payment attempts for tenants’ own subscription invoices. |
| Tenant | 0117_customer_invoice_lines |
Invoice lines and totals, and the link between a credit sale and its invoice. |
| Tenant | 0118_payments_hardening |
The Under review payment status and the safeguards that stop a payment being credited twice. |
Run the global migration, then the tenant one, against the shared database and every dedicated tenant database:
make migrate-global-upmake migrate-tenant-upSettlement and fee changes ship in their own migrations and are covered with settlements and fees.
2. Set the environment
Section titled “2. Set the environment”| Variable | Set on | Why |
|---|---|---|
PAY_BASE_URL |
API, scheduler | Public origin of the pay app. Invoice links and pay buttons are built on it. See deploying SaleSprint Pay. |
PUBLIC_API_BASE_URL |
API | This API’s public origin as a payment gateway can reach it, without /api/v1. It builds the webhook URL shown on each gateway configuration. Left empty, that field is blank. |
PAYMENTS_SECRET_KEY |
API, scheduler | Base64 32-byte key that encrypts stored gateway credentials. Falls back to the external-login key when unset — set a dedicated one in production. |
DOCUMENT_LINK_SECRET |
API, scheduler | Key customer links are derived from. Rotating it invalidates every link already sent. |
3. Deploy the pay app
Section titled “3. Deploy the pay app”4. Register the gateway webhooks
Section titled “4. Register the gateway webhooks”Each gateway configuration shows a read-only Webhook URL:
{PUBLIC_API_BASE_URL}/api/v1/webhooks/payments/{gateway}where {gateway} is paystack, payswitch or hubtel.
- One URL per gateway serves every tenant. Paste it once into that provider’s
dashboard. Notifications are matched to the tenant and the payment by the payment’s own
reference, so no tenant ID is needed. The older
?tenant=form still works. - The same URL carries notifications for tenants’ subscription payments.
- A notification is only a nudge. SaleSprint never settles from the notification body; it asks the gateway about that payment and settles from the gateway’s answer.
5. Make sure the scheduler is running
Section titled “5. Make sure the scheduler is running”The scheduler (cmd/scheduler) is what makes payments reliable when a notification is
late or lost. It:
- re-checks every open payment attempt with its gateway — merchant collections and subscription payments alike;
- retries notifications that arrived while a payment was still pending;
- expires requests nobody paid, after confirming with the gateway that nothing was paid.
Without it, a payment whose notification never arrives stays pending until the customer returns to the payment page.
Two scoped settings shape this, both under Payments:
payments.payment_intent_expiry_minutes (how long a payment request stays valid) and
payments.merchant_collection_hold_hours (how long collected money waits before it
becomes available).
6. Convert existing credit sales
Section titled “6. Convert existing credit sales”Credit sales completed after the upgrade raise their invoice automatically. Open credit sales from before it keep working, but have no invoice — so no reminders, no pay link, and they are not in the Customer Invoices list — until they are converted.
The conversion is an API call, made once per tenant by a user of that tenant who is allowed to create customer invoices:
POST /admin/customer-invoices/backfill-from-sales(under the API’s usual /api/v1 prefix — see the API reference). It
answers with how many invoices it raised and whether more remain:
{ "created": 200, "remaining": true }- It handles up to 200 sales per call. Repeat while
remainingistrue. - It is safe to run again: a sale that already has an invoice is skipped.
- Each open credit sale gets an invoice numbered from the tenant’s normal invoice sequence, with a due date from the customer’s payment terms. Payments already recorded against the sale are kept.
- Fully paid credit sales are left alone.
- Customers are not emailed. These are debts the customer already knows about.
- A sale that cannot be converted is skipped and logged; the rest carry on.
What merchants will notice
Section titled “What merchants will notice”Worth passing on to your tenants:
- Creating an invoice with a status other than Draft is now refused.
- A stock-tracked product on an invoice raised in finance is refused
(
stocked_product_requires_sale). - A credit sale past the customer’s credit limit is refused (
credit_limit_exceeded) unless the user holdssales.sale.credit_limit_override. Grant that to supervisors before go-live, or tills will be stopped by limits nobody enforced before. - Cancelling a credit sale whose invoice has payments applied is refused
(
invoice_has_payments). - A payment cannot be marked completed by hand, and a payment whose amount or currency does not match is held as Under review until an operator resolves it.
- Tenants paying their own subscription invoice by card are sent through the gateway’s redirect page; the payment is verified against an attempt the server created.