Skip to content

Billing & Invoices

The Billing page is the cashier’s desk of ProxPanel. Every charge a subscriber incurs — first month, renewal, plan change, top-up, refill, prepaid card redemption — becomes a row in the invoices table. From here you create invoices manually, post payments (full or partial), regenerate PDFs in any of 17 paper sizes, send the receipt by WhatsApp, consolidate multiple unpaid invoices into one, and reconcile reseller commissions.

Invoices are first-class records: each carries a public token that lets the subscriber view or print their receipt without logging in (used by the QR code on the PDF), and a JSONB notification_log so you can see exactly which channels — email, SMS, WhatsApp — were used to deliver each copy.

  • SidebarInvoices (receipt icon).
  • Direct URL: /invoices.
  • From any subscriber’s detail page → Billing tab.
  • From the Dashboard’s Unpaid Invoices card.

A subscriber’s reseller can view and act on their own subscribers’ invoices; admins see everything. The invoices.view / invoices.create / invoices.edit permissions gate access for resellers without those flags set.

ColumnDescription
NumberAuto-generated invoice number (configurable prefix + zero-padded sequence).
SubscriberName + username. Click to open the subscriber.
Issued / Duecreated_at and due_date. Overdue rows are red.
Total / Paid / BalanceThree currency columns. Balance > 0 → still owed.
Statuspending, partial, paid, cancelled, refunded.
ChannelsPills showing which notification channels delivered the invoice.
ActionsView, PDF, Send WhatsApp, Add Payment, Delete.

Filters at the top let you narrow by status, date range, reseller, subscriber, and amount range. The top toolbar surfaces Create Invoice, Consolidate Pending, and Export CSV.

The POST /api/invoices handler accepts a subscriber ID, due date, optional notes, and a list of line items. Each item carries a description, quantity, unit price, and an optional tax percentage; the handler computes subtotal, tax, and grand total server-side so the totals shown in the UI always match what is stored. Currency, tax label, and decimal precision come from the Billing tab in Settings — they apply to every invoice the system produces.

Two flows generate invoices automatically — you rarely create them by hand:

  • A subscriber renews → the renewal handler creates a new or renewal transaction and an invoice with status=pending for the renewed amount.
  • A subscriber changes service plan → the prorated charge or credit becomes its own line item on a new invoice.

When you do create one manually, the form pre-fills the subscriber’s last plan price and lets you add ad-hoc items (installation, equipment sale, late fee).

GeneratePDF does not render the PDF from scratch. Instead it points headless Chromium at the same web invoice view the operator sees (http://proxpanel-frontend/invoices/:id/print?token=...), waits for the page to settle, and prints to PDF. This guarantees the PDF is pixel-identical to the web version and updates automatically when you change the invoice template.

Selectable paper sizes in Settings → Billing → Invoice PDF:

RegionSizes available
ISO AA3, A4, A5, A6
ISO BB4, B5
USLetter, Legal, Tabloid, Executive
Thermal / receipt58 mm, 80 mm
CustomWidth × height in mm

Chromium runs with --no-sandbox --disable-gpu --hide-scrollbars --print-to-pdf-no-header --virtual-time-budget=5000. The virtual-time budget gives JavaScript (charts, QR rendering) up to 5 seconds to finish before the snapshot. For the consolidated-invoice page, which embeds multiple sub-invoices in iframes, the budget is raised so child frames finish loading.

Each invoice has a public_token — a random 32-byte URL-safe string generated at creation time. The QR printed on the PDF encodes:

https://your-panel-host/p/invoice/<id>?token=<public_token>

The corresponding public route (GET /invoices/public/:id) accepts the token, validates it with a constant-time compare to defeat timing attacks (audit 2026-05-11), and serves the invoice without requiring authentication. Subscribers scan the QR and see the receipt instantly — convenient for cash collectors who hand the PDF over after collecting payment.

If you suspect a leaked token, click Regenerate Token on the invoice — the old URL stops working and a new QR is printed.

POST /api/invoices/:id/payments records a payment row. Partial payments are first-class:

FieldEffect
amountDecremented from invoice.balance.
methodcash, card, bank_transfer, prepaid_card, collector, online.
referenceFree-text (cheque #, transaction ref).
paid_atDefaults to now; can backdate.

After the payment is saved:

  1. If balance reaches zero → status is set to paid.
  2. If still > 0 → status becomes partial.
  3. A matching transactions row is inserted (income side) so revenue reports stay in sync.
  4. If the invoice is for a renewal and is now fully paid → autoRenewSubscriber() extends the subscriber’s expiry by the service period.

The QuickPay action on the subscriber detail page does the same in one click — useful when a customer walks in to pay an exact balance.

Prorated plan changes — auto-credit / auto-charge

Section titled “Prorated plan changes — auto-credit / auto-charge”

When a subscriber changes service mid-cycle, the CalculateProrata endpoint determines whether to credit them for unused days on the old plan, charge them for the remaining days on the new plan, or both.

Formula:

unused_days = (expiry_date - today) clamped to [0, service_period]
old_credit = (old_price / service_period) × unused_days
new_charge = (new_price / service_period) × unused_days
prorate = new_charge - old_credit

If prorate > 0 → an invoice is created for the difference. If prorate < 0 → a credit-note transaction is posted to the subscriber’s account ledger. The UI shows the breakdown before the operator confirms, so the customer sees exactly why they owe (or are owed) what they are.

Customers often accumulate a handful of small unpaid invoices over a few months. ConsolidatePending rolls them into one:

  1. All pending and partial invoices for the subscriber are gathered.
  2. A new parent invoice is created with their original line items copied in.
  3. Each source invoice is marked cancelled and linked to the parent via consolidated_into_id.
  4. The new invoice carries one combined balance; the audit trail to the source invoices is preserved for accounting.

The collector can now hand the customer a single PDF instead of five.

SendInvoiceWhatsApp posts to the Ultramsg-compatible gateway configured in Communication Rules. The handler:

  1. Calls GeneratePDFToFile to render the PDF to a temp path.
  2. Uploads the file to the gateway as a media attachment.
  3. Sends a template message (configurable per panel) that includes the public receipt URL.
  4. Appends to the invoice’s notification_log JSONB: timestamp, channel=whatsapp, status=sent or failed, gateway response, and the operator who triggered it.

If WhatsApp is unconfigured or the gateway returns an error, the failure is logged but the invoice itself is unchanged. The operator sees the error toast immediately.

SendCombinedInvoice does the same for a consolidated parent invoice — one message, one PDF, all sub-invoices included.

GetCommissions returns a per-reseller breakdown for a chosen date range:

ColumnSource
Resellerresellers table
Invoices issuedcount of invoices where reseller_id = ? and created_at in range
Total billedsum of total
Total collectedsum of paid_amount
Commission ratereseller.commission_percent
Commission earnedtotal_collected × commission_percent / 100
Already paid outsum of transactions.type='commission_payout'
Outstandingearned − paid out

Click Mark Paid on a row to post a commission_payout transaction; the reseller’s balance is reduced and the outstanding amount drops to zero. The audit log records the payout under your username.

Collect a partial payment and finish billing tomorrow

Section titled “Collect a partial payment and finish billing tomorrow”
  1. Open the invoice, click Add Payment.
  2. Enter the amount the customer is paying today, method = cash, reference = receipt book number.
  3. Save. Invoice status flips to partial and the balance updates.
  4. The next day, repeat with the remaining amount. Status flips to paid and the subscriber’s expiry is extended.
  1. Open the subscriber → click Change Service → pick the new plan.
  2. Tick Calculate proration. The dialog shows old credit, new charge, and net amount.
  3. Confirm. The plan is changed, the prorated invoice is generated, and (if the subscriber is online) a CoA disconnect forces them to reconnect on the new pool.
  4. Add the payment when the customer pays.

Roll five unpaid invoices into one for collection

Section titled “Roll five unpaid invoices into one for collection”
  1. Open the subscriber → Billing tab.
  2. Click Consolidate Pending. Confirm the prompt.
  3. A single new invoice replaces the five originals (which are cancelled and linked).
  4. Click Send WhatsApp to deliver the consolidated PDF + receipt URL.
PermissionWhat it gates
invoices.viewSee the Invoices list and detail pages.
invoices.createCreate manual invoices and consolidate pending ones.
invoices.editEdit due dates, line items, and post payments.
invoices.deleteSoft-delete an invoice. Refunded invoices remain in the database.
transactions.view_allSee all invoices system-wide, not only the reseller’s own.

Resellers without invoices.view_all see only invoices for their own subscribers. Sub-resellers see only their parent’s subscribers when permitted.

  • Cash Collection — collectors mark invoices paid in the field and trigger auto-renewal.
  • Reports — Revenue, Transaction, and Reseller Performance reports source their numbers from the invoices and transactions tables.
  • Communication Rules — configures the WhatsApp gateway and templates used by SendInvoiceWhatsApp.
  • Settings → Billing — currency, tax rate, invoice number format, paper size, bank details printed on PDFs.
  • Subscriber detail — per-subscriber billing tab and QuickPay.