This article covers PortaPro's per-trip billing model — the way front-load dumpster service, pump service, and any other "recurring pull" work is billed. If your customer has containers that get serviced on a schedule (weekly, twice-weekly, day-of-week specific) and you want their monthly invoice to say "4 pulls included in base rate, 2 extra pulls at $75 each," this is the workflow.
If you're doing roll-off *terms* (delivery, swaps, pickup, one final statement) see the Roll-Off Swap Cycle Workflow. If you're doing recurring portable-toilet rentals, see the Long-Term Rental Workflow — the two workflows share the Rate Card infrastructure, but per-trip billing has extra machinery for allowances, extra pulls, and late-pull labeling.
---
FTL-1: What Per-Trip Billing Does
The one-line summary
Per-trip billing counts how many times a unit at a customer's site was *serviced* (pulled, dumped, pumped) during the billing cycle and prices it: first N pulls are included in the base rate at $0, extra pulls beyond N bill at the per-pull rate, and any pulls that should have been in the previous cycle but arrived late get their own labeled line.
Why the split matters
"You got 6 pulls this month and pay $600" isn't what the customer wants to see. They want to see:
This is how the industry has priced front-load service for decades. PortaPro renders every rate card row that has an allowance this way automatically.
When to use per-trip vs. cycle rental
| | Per-trip | Cycle rental |
|---|---|---|
| Line items | Included / extra pulls / late pulls | One flat line per period |
| Rate card field | included_pulls_per_cycle set | included_pulls_per_cycle = 0 |
| Best for | Front-load, pump service, dumpster service | Portable toilets, generator rentals, ongoing units |
Set the allowance on the Rate Card row and PortaPro switches modes. No global setting, no separate menu — it's a per-row decision.
---
FTL-2: Configure a Per-Trip Rate Card Row
Step 1: Open the site's Rate Card
Step 2: Fill in the row for per-trip billing
Step 3: Save
That's it. From the next cycle forward, PortaPro:
The day-of-week generator (Phase A)
When a per-trip row has a service_day_of_week set, PortaPro's nightly job generator creates a service job for every occurrence of that weekday in the coming week. So a Tuesday+Friday row on a bi-weekly site creates two jobs each week automatically. Completing those jobs feeds the pull count.
The dedup key (auto_generated_from_recurring_unit_id, auto_generated_for_date) ensures the nightly cron never spawns a duplicate — a row change won't re-open an already-serviced date's job.
---
FTL-3: The Allowance Math (What You'll See on Invoices)
Say the customer's Rate Card has a per-trip row: 1 front-load dumpster, included_pulls_per_cycle = 4, rate $75.
Cycle with exactly 4 pulls: PortaPro emits one $0 line:
Cycle with 6 pulls: PortaPro emits two lines:
Cycle with 3 pulls (under): PortaPro emits one $0 line:
Under-consumption doesn't credit the customer — that's what "included" means. If you want under-consumption to credit the customer, add a manual credit; PortaPro won't do it automatically because industry pricing treats included pulls as bundled.
Late pulls from previous cycle
If a service was completed with a scheduled date *before* the current cycle's window opened (paperwork lag, driver marked a Feb 27 pull complete on Mar 3), that pull is a late pull from the previous cycle and gets its own labeled line:
Late pulls do not consume the current cycle's allowance — they're an explicit catch-up. If they did, one late pull would eat a customer's Mar-1 allowance slot and shove a Mar-4 pull into overage.
The Rate Card row id stamp
Every emitted line stamps invoice_items.source_rate_card_row_id back to the row that produced it. This lets PortaPro's cycle-close tools:
---
FTL-4: Generate the Cycle Invoice
Manual cycle generation:
Automatic cycle generation happens on the schedule you set in Settings → Finance → Cycle Automation.
What the operator warning about "extra pulls" surfaces
At cycle-close time, PortaPro shows a review flag if any per-trip row went over its allowance for the cycle:
This is only informational — the invoice generates normally either way. The flag lets you check the pattern before you send.
---
FTL-5: Chronological Pull Splitting (Why It's In Date Order)
buildPerTripRowLines sorts the cycle's completed pulls by scheduled date before splitting. So "the first 4 included" always means the first 4 chronologically, not the first 4 by ID or by weight.
This matters when a customer disputes: you can point at the invoice's date range in each line ("included: Mar 4 – Mar 18") and prove exactly which pulls were included and which were overage.
If two pulls fell on the same date (a make-up + a scheduled together), the tiebreaker is job id — deterministic, but rarely visible.
---
FTL-6: Editing a Rate Card Without Losing Attribution (Phase G Fix)
Every Rate Card edit used to delete every row for the site and re-insert them. The row ids rotated every save. Downstream tables that referenced those ids (jobs.auto_generated_from_recurring_unit_id, maintenance_reports.recurring_unit_id, invoice_items.source_rate_card_row_id) all lost their links, which caused:
Fix (July 2026, Phase G): the RPC is now an id-preserving upsert. Editing a row updates it in place; only genuinely new rows insert; only rows the operator actually removed delete. Row ids survive.
There's also a partial-writer clobber fix: multiple internal writers (rate modal, day-of-week feature, billing dates feature) each send *some* of the row's columns. The old RPC nulled anything a writer didn't send. Now, a writer omitting a column keeps the existing value — so editing the rate doesn't wipe the weekday anchor, and editing the day-of-week doesn't wipe billing dates.
You won't see this fix in the UI. You'll see its absence: the nightly generator behaves normally after edits, cycle-close top-up matches work, and historical attribution stays intact.
---
FTL-7: Offline Service Marking + RPC Attribution (Phase B)
Drivers on-scene without cell signal can still complete service. The mobile app queues the completion in IndexedDB and syncs when signal returns.
What lands correctly in the sync:
This matters for the late-pull line above (FTL-3): the sync's timestamp isn't the pull's date. A driver who marks a Feb 27 pull complete on Mar 3 gets it attributed to Feb 27 correctly.
---
FTL-8: Skills Routing for Front-Load (Phase E → Phase F)
Front-load routes are picky: a route needs a rear-loader capable of front-load lift, a driver certified on the equipment, sometimes a specific yard load-out. PortaPro's route planner reads two matching tables to decide:
Phase F extended this so the planner treats sanitation and containers as separate skill families — a driver certified on portable-toilet service isn't auto-assigned to a front-load run and vice versa unless they're certified on both. This prevents the classic mis-route where a portable-toilet driver gets a container job they don't have the truck for.
You configure skills in Settings → Team → Skills and per-vehicle in the vehicle detail drawer. New drivers default to their trained skills; edit as they gain certifications.
---
FTL-9: Skipped Job Status
Sometimes a driver arrives and can't complete the service — the gate's locked, the customer moved the container, the address is wrong. Marking the job completed would consume the customer's allowance for a pull that never happened. Marking it cancelled loses the attempt history.
PortaPro has a distinct skipped status for this:
If the skip results in a dry-run fee (customer's fault: they forgot to unlock the gate), attach a manual fee to the job and it sweeps into the cycle invoice per Scenario 5.
---
FTL-10: Cycle-Close Visibility (Phase D)
Before you close a cycle, PortaPro surfaces a pre-close review panel showing:
The panel is informational — you can generate cycle invoices without it — but it lets you catch data problems the last two months looking at Stripe's dashboard would have missed.
---
FTL-11: What Broke in the Merge (R2 Fix, July 2026)
Full transparency: earlier in July, a branch merge into dev-staging accidentally reverted the entire per-trip allowance loop to its pre-allowance version. The main invoice generator lost its calls to buildPerTripRowLines — every cycle invoice would have been billed as one giant line at the per-pull rate with no allowance split, no included lines, no late-pull labels.
Worse: the code was still trying to *report* the extra-pull totals ("perTripExtraPulls" / "perTripOverageAmount") in its return block, but those accumulators had been dropped from the loop. So cycle invoices would have written to the database successfully, then thrown a ReferenceError at the return statement, showing the operator a failure toast on an invoice that actually got created.
The R2 fix: the per-trip loop was recomposed around buildPerTripRowLines (the same helper the draft top-up path uses, so the two never disagree). The counters, the unpricedSites warning, the source_rate_card_row_id stamp, and the chronological staging were all restored. As of the July 2026 dev-staging deploy, this is bit-identical to the pre-merge behavior plus the allowance split, plus the late-pull line.
You won't have seen this because it was caught and fixed in the same reconciliation pass before it could ship to production. Documenting here so if a customer asks "why did my March invoice look different from April's" the answer is available.
---
FTL-12: Troubleshooting
"The cycle invoice for a per-trip customer just shows one big line, no included / overage split."
The Rate Card row's included_pulls_per_cycle is 0 or missing. Set it to the allowance number and regenerate.
"The 'extra pulls beyond N' line is showing on a customer who was under allowance."
Under-consumption doesn't create an overage line — verify the pull count with the driver's completed jobs. If you're seeing an overage line for 0 or under, that's a bug; report it.
"Late-pull line showed up on a fresh customer with no previous cycle."
A pull is late if scheduled_date < cycle window start. Check that the customer's cycle billing_start_date is set correctly; if a driver completed a pull for a date before the cycle even opened, it's technically "late from before the account started." Adjust the pull's date, or accept the labeling.
"The nightly generator created duplicate service jobs after I edited the Rate Card."
This was the pre-Phase-G bug. Update to the latest deploy (post-July-2026). If you still see it, the dedup key (auto_generated_from_recurring_unit_id, auto_generated_for_date) isn't preserving through the RPC — verify the migration 20261008_01 is applied.
"I edited a row's rate and the day-of-week got wiped to null."
Same pre-July-2026 bug, partial-writer clobbering. Update to the latest deploy. Post-fix, the RPC preserves omitted columns.
"A driver marked a pull skipped but the customer got charged for it."
Skipped pulls do NOT bill. If it billed, verify the job's actual status — some operators write "skipped" in notes but mark the job completed. Only the enum status is authoritative.
"The pre-close review says '3 unpriced sites.'"
Those sites have completed jobs but no matching Rate Card row (or the row's product_id doesn't match the job's product). Open each unpriced site, either create the missing row or fix the product mismatch, then regenerate. Unpriced sites bill at $0 on the invoice — a silent revenue leak if you don't fix them.
"The operator warning says '2 extra pulls' but the invoice shows 3."
The counters are refreshed on generation. If the review panel shows a stale count, click Refresh or regenerate; the panel counts from the last generation, not real-time.
"A driver-skills mismatch blocked a dispatch."
Check the driver's driver_skills rows and the vehicle's skills array. For a container job, both need containers; for portable-toilet service, both need sanitation. If both parts of the pair have the right skill and the planner still refuses, the job's product may be miscategorized — the routing engine reads products.product_type to decide the required skill.
---
