This article covers cycle close — the process where PortaPro turns a customer's month (or week, or 28-day period) of activity into one invoice, applies any credits, sweeps in job fees and late charges, charges autopay if it's on, and hands you a review panel so you can catch problems before you send. Cycle close is the connecting glue between every other workflow: long-term rentals (Scenario 1), roll-off (Scenario 3), per-trip billing (Scenario 4), and emergency fees (Scenario 5) all funnel through here.
This is the operator's most important billing surface. Read it slowly.
---
CYC-1: What Happens at Cycle Close
One customer at a time, PortaPro:
Any single one of these steps failing degrades gracefully — the invoice still generates. Failures show up as review flags so the operator can catch them.
---
CYC-2: Manual vs. Automatic Cycle Generation
Automatic: on the schedule you set in Settings → Finance → Cycle Automation. PortaPro walks every eligible customer at that time and generates each one's cycle invoice.
Manual: from a customer's Billing tab, click Generate Cycle Invoice, pick the billing period, and generate. Useful for:
Both paths use the same generators. Same math, same flags, same output.
Cycle length and anchor
Set globally in Settings → Finance → Cycle Automation, overridable per customer:
The customer inherits the org default unless they have a per-customer override.
---
CYC-3: Proration for Mid-Cycle Rows
Not every Rate Card row is active for the whole billing window. Sites activate mid-month; sites close mid-month; specific units start or stop early. prorationForWindow handles this.
How it works:
Examples (billing window = Mar 1–31, 31 days):
The proration tail on the invoice
When a row prorates, PortaPro appends "(prorated 20/31 days)" to the line description so the customer sees the math. If they dispute, you can point at the exact day counts.
Flat one-time charges never prorate
A one-off flat line item (e.g. a delivery fee) always bills at its full amount. Only per-period cycle lines prorate.
The review flag
`{ kind: "prorated_lines", count: N }` — stamped on any cycle invoice with prorated rows. Shows on the review panel as a chip so operators know some lines are partial.
---
CYC-4: Rate Basis — When to Use Which Rate
If you raised the rate on a customer's Rate Card row on Mar 15, does their March invoice bill at the old rate, the new rate, or a mix?
PortaPro supports two org-wide rate bases (Settings → Finance → Rate Basis):
Which to pick:
Rate history collection
When rate basis is `cycle_start`, PortaPro reads slru_rate_history — a table populated by triggers on rate-affecting column changes on service_location_recurring_units. Every edit snapshots the prior card. Since history collection was added in Phase 3G-2, `cycle_start` becomes fully accurate one cycle after enablement — before enablement, there's no history for old rates, so PortaPro falls back to the live card. If you switch from `invoice_date` to `cycle_start` today, next month's cycle uses the correct rate; this month's may show today's rate if there's no history yet.
Documented limitation, not a bug. Enable rate basis when convenient and let one cycle pass before relying on it for disputes.
---
CYC-5: Customer Credits
If a customer has credit on file (overpayment, goodwill credit, refund credit note), PortaPro applies it automatically at cycle close.
Where credits come from
How they're applied
At cycle close, PortaPro:
Credit application is post-tax — the invoice subtotal, tax, and total all show the pre-credit numbers; the payment row shows the credit taken.
The review flag
`{ kind: "credit_applied", amount: X }` — stamped on any cycle invoice that consumed credit. Shows on the review panel with the dollar amount.
If credit exceeds the invoice
The credit application is capped at the invoice's remaining balance. Excess credit stays in customer_credits and applies to the *next* cycle. Credits don't create negative invoices.
---
CYC-6: Autopay Charge on Cycle Send
If the customer has cycle autopay enabled AND a valid saved payment method, PortaPro attempts an off-session Stripe charge on the customer's card when the cycle invoice sends.
Flow:
The review flag
`{ kind: "autopay_failed", error: "…" }` — stamped when the auto-charge fails. Shown on the review panel with the Stripe error message. Operator handles the retry (usually by clicking Charge on File after the customer updates their card).
Prerequisites
If any of those miss, the invoice sends but doesn't auto-charge. See Long-Term Rental Workflow, LTR-7 for setting up autopay.
The autopay overlap flag
Some customers have both a legacy recurring_invoices schedule AND cycle autopay. If both are active, PortaPro stamps `{ kind: "autopay_overlap" }` so the operator can retire the legacy schedule — otherwise the customer could be double-billed on one cycle.
---
CYC-7: The Pre-Close Review Panel
Every cycle invoice writes a review_flags JSONB array capturing anything unusual — 17 flag types in total. The review panel renders these as chips.
The full flag catalog
Money-affecting (usually needs a look):
Volume / activity (usually informational):
Sweep signals:
Rate / basis anomalies:
Each flag renders as a short chip on the panel; hover shows the count or amount.
What the panel does
---
CYC-8: Top-Up on Draft Invoices (Mid-Cycle Draft Editing)
Sometimes a driver completes a per-trip service between cycle-invoice generation and cycle-invoice send. If you already generated a draft invoice for that customer but haven't sent it yet, you don't want to delete + regenerate — you want the new pull to appear on the existing draft.
That's topUpSingleDraft:
The critical guarantee: topUpSingleDraft uses the same pure helper (buildPerTripRowLines) as the original generator, so they can never disagree on the included / extra / late split.
When to use top-up:
When NOT to use top-up:
---
CYC-9: Late-Charge Carryover
If a service report from the previous cycle wasn't billed at the previous cycle-close, PortaPro carries it to the current cycle as a late-line — a separately-labeled line at the per-pull rate, distinct from the current cycle's included / extra-pulls split (see Front-Load Recurring, FTL-3).
The review flag `late_usage_carried` counts these.
When late-charges appear:
---
CYC-10: Manual Adjustment Before Send
The pre-close review panel is informational, but the draft invoice itself is editable. You can:
Editing an already-sent invoice requires a separate flow (delete + regenerate, or issue a credit note).
The "billed charge recompute blocked" flag
If you generate a cycle invoice, send it, then edit the Rate Card row that generated one of its lines, PortaPro cannot retroactively recompute the billed charge. It stamps `billed_charge_recompute_blocked` on the invoice so operators know the line reflects old data. Handle by issuing a credit or (if unpaid) deleting and regenerating.
---
CYC-11: How the R2 and R3 Fixes Show Up Here
Full transparency (as with prior scenarios). Both fixes landed in this scenario's territory:
R2 (per-trip loop recomposed) — the merge broke generateRateCardInvoice's per-trip emit loop, but the R2 fix restored the allowance-aware version. All per-trip cycle-close behavior described in CYC-1 step 4 depends on R2 being in place. Post-July-2026, the loop uses buildPerTripRowLines, the counters increment correctly, and the return statement doesn't ReferenceError. See Front-Load Recurring, FTL-11 for the plain-language explanation.
R3 (fee sweep wired) — pre-fix, sweepJobFeesToInvoice had zero callers. Every "next-cycle fee lands on the cycle invoice" promise (CYC-1 step 6) was broken. Post-R3, both generators call the sweep before applyDocumentTax, and the job_fees_swept review flag surfaces on the panel. See Emergency & On-Call, EMR-4 for the plain-language explanation.
Neither reached production — caught in the reconciliation pass. Documenting so if you dig into a March 2026 invoice with weird numbers, the answer is here.
---
CYC-12: Troubleshooting
"The cycle invoice generated but has no fees / no pulls / no roll-off charges."
Check the review flags. If job_fees_swept count is 0, no eligible fees were queued (check the source jobs' surcharge_applications); if swept_job_charges count is 0, no completed roll-off/manual/dry-run jobs matched. Verify each source: fee disposition, job status, rental_term_id presence.
"A row shows factor 0 and got skipped."
Its effective window doesn't overlap the billing window. Verify billing_start_date / billing_end_date — if either is set to a date outside the cycle, the row is intentionally excluded. Un-set the dates or move them into the window if it's wrong.
"The customer's rate changed mid-cycle and the invoice shows the new rate, but I wanted the old rate."
Your org's cycle_rate_basis is invoice_date. Switch to cycle_start in Settings → Finance → Rate Basis. Note the one-cycle-lag caveat in CYC-4.
"The prorated line shows (prorated X/Y days) but the numbers don't match my mental model."
PortaPro uses inclusive-day math (Mar 1 through Mar 3 = 3 days, not 2). Also, the "window days" is inclusive too — Mar 1 through Mar 31 = 31 days. Recheck with inclusive counting.
"Autopay didn't charge but the customer has a card on file."
Check the autopay_failed flag on the invoice — the Stripe error is embedded. Common causes: card expired, card decline, Stripe Connect account issue on your side, customer's Stripe customer_id not linked. Fix and use Charge on File manually.
"Two customers with autopay both had it fail this cycle."
Check the org-level Stripe Connect status. If your account was suspended or reauthorization is needed, ALL autopay attempts fail until it's restored. Check Settings → Payments → Stripe Connect.
"The 'autopay overlap' flag appeared for a customer."
This customer has both a legacy recurring_invoices auto-charge schedule AND cycle autopay enabled. Both would attempt to charge the customer at cycle close, potentially double-billing. Retire the legacy schedule to avoid potential double billing — Settings → Customer → Legacy Billing.
"Credit applied but the invoice still shows a balance."
The credit balance was less than the invoice total. The remaining balance = invoice total − credit applied. The customer owes the remainder. To see the credit's dollar amount, check the payment row (credit_note payment method) or the review flag details.
"A per-trip line is missing pulls."
Two possibilities: (1) the pulls belong to a different Rate Card row (same product, different site or different setup) — check source_rate_card_row_id on the completed jobs; (2) the pulls' scheduled dates fall outside the billing window and got carried as late-lines. The late_usage_carried flag counts the latter.
"Top-up added a duplicate line for the same pull."
Should not happen — topUpSingleDraft matches by source_rate_card_row_id. If duplicates appear, verify the invoice items have unique row-id stamps; the pre-Phase-G row-id rotation could have de-linked them from their source. Refreshed row ids fix this going forward.
"I sent a cycle invoice and now the customer wants to change their address / cycle length / anything."
Sent invoices are frozen for accounting integrity. Any change is a new invoice or a credit note against the sent one. Don't edit sent invoices in the database — it breaks the ledger.
---
