The billing engine aggregates matched billable CDRs for a time period, generates invoices with line items, creates PDF invoices, and marks CDRs as invoiced.
https://billing.yealin.com.au/billing-runsTOKEN=$(curl -s -X POST https://billing.yealin.com.au/api/auth/token/ \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"YOUR_PASS"}' \
| python3 -c "import sys,json; print(json.load(sys.stdin)['access'])")
curl -s -X POST https://billing.yealin.com.au/api/billing-runs/execute/ \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"period_start": "2026-05-01", "period_end": "2026-05-31"}'
For each active customer:
1. Find all CallRecords where:
- customer = this customer
- billable = True
- invoiced = False
- start_time between period_start and period_end
2. Group by destination type
3. Calculate:
- Voice charges: (duration_minutes × sell_rate_per_min) + connection_fee
- Service charges: fixed charges from ServiceCharge model
4. Create Invoice with line items
5. Calculate subtotal, GST (10%), total
6. Generate PDF via WeasyPrint
7. Mark all included CDRs as invoiced = True
8. Invoice status = 'draft'
Invoices use the format YC-YYYY-NNNN:
YC — Yealin Communications prefixYYYY — year of issueNNNN — sequential number within the year (zero-padded to 4 digits)Examples: YC-2026-0001, YC-2026-0042, YC-2027-0001
| Status | Description |
|---|---|
| draft | Created by billing run, not yet sent |
| sent | Email sent to customer |
| paid | Payment received (Stripe or manual) |
| overdue | Past due date, not paid |
| void | Cancelled/voided |
https://billing.yealin.com.au/invoicesIf the PDF is corrupted or missing:
TOKEN=$(...)
curl -s -X POST https://billing.yealin.com.au/api/invoices/INVOICE-UUID/regenerate-pdf/ \
-H "Authorization: Bearer $TOKEN"
For bank transfers, cheques etc:
https://billing.yealin.com.au/paymentsGST is calculated as 10% of the subtotal:
Non-taxable line items (credits, adjustments) can be configured per line item.
PDFs are generated using WeasyPrint and stored at:
/var/www/yealinbilling/media/invoices/YC-2026-0001.pdf
The PDF includes:
Note: Charts in PDFs use inline SVG — never use JavaScript charting libraries as WeasyPrint cannot render JavaScript.
When a CDR (call record) arrives from Twilio, the system immediately classifies it to a destination based on the phone number prefix — AU Mobile (+614), AU National QLD (+617), etc. This destination is stored permanently on the CDR record and determines the billing rate.
The problem: If the destination map was incomplete or wrong at the time the CDR was ingested, the CDR gets permanently stuck on the "International (Catch-all)" destination and gets billed at the wrong rate ($0.35/min instead of e.g. $0.22/min for AU Mobile calls).
Use the Reclassify Catch-all button on the Sync page any time you:
When Campbell's subaccount (Campbells Legal Services) was first synced, 3,526 CDRs were assigned to "International (Catch-all)" because the destination prefixes for +614 (AU Mobile) and +617 (AU National QLD) had not yet been added to the database at the time of ingest.
When billing ran for February 2026, Campbell's invoice came to ~$800 — nearly 3x the correct amount — because all his AU Mobile calls were being charged at the international catch-all rate of $0.35/min instead of $0.22/min.
After clicking Reclassify Catch-all, 3,526 CDRs were correctly reassigned to AU Mobile, AU National QLD, etc. The February invoice dropped from ~$800 to $263.
POST /api/cdr/reclassify-catchall/
Returns:
{
"status": "complete",
"total_catchall": 3526,
"reclassified": 3526,
"remaining_catchall": 1994,
"message": "Reclassified 3526 of 5520 catch-all CDRs. 1994 remain (genuinely international)."
}