This article covers how PortaPro handles emergency, after-hours, and on-call work — the phone-rings-at-9pm jobs where a customer needs immediate service and expects the invoice to reflect the urgency. It also covers priority jobs more generally, damage / dry-run / after-hours fees, and unit replacement when something breaks in the field.
If you're doing regular scheduled service, see the Front-Load Recurring Service or Long-Term Rental workflows. This article is for the exceptions — the runs that fall outside the normal cycle rhythm.
---
EMR-1: The Core Model
What "emergency" means in PortaPro
A one-off service job — usually created immediately, sometimes scheduled for later the same day — that carries an extra fee for the urgency (or the after-hours nature, or the hazmat aspect, or whatever your ops charges for). The fee is attached to the job itself, not to a separate invoice, so it rides through billing correctly whether the customer is cycle-billed or one-off.
The two things every emergency job needs
That's the whole model. Everything else (priority flagging, autopay handling, cycle sweep, tax treatment) flows from those two things automatically.
Two ways the fee bills, chosen per fee
When you attach a fee, you pick its billing disposition:
Every fee stamps its choice at attach time. Sweep-eligible fees are marked next_cycle and live in the surcharge ledger; immediate-billing fees are marked immediate and flow through the invoice generator directly.
---
EMR-2: The Priority Toggle
Every job has a Priority switch in its detail modal (and in the wizard's scheduling step when you create it). Flipping it on marks the job is_priority = true.
What Priority does:
What Priority does NOT do:
Per-request only
Priority is set per job at create time or toggled later. There's no "always mark this customer's jobs priority" setting — that would silently apply premium pricing to every job for that customer, which is a footgun. If a customer routinely needs premium service, attach the emergency fee at job-attach time as your standard practice.
---
EMR-3: Attach a Fee to a Job
Step 1: Open the job's detail view
Step 2: Add the fee
What the Surcharges catalog stores:
Configure the catalog in Settings → Finance → Surcharges & Fees. Once a fee is in the catalog it's available on every job.
Snapshot semantics
When you attach a fee, PortaPro snapshots the name, type, and amount onto the surcharge_application row. Later changes to the catalog entry don't retroactively alter fees already attached — the customer sees the price they agreed to at attach time.
---
EMR-4: How next_cycle Fees Actually Reach the Invoice
This is the piece that used to be broken and got fixed in July 2026. Full transparency.
When you attach a next_cycle fee to a job, it sits in surcharge_applications with billing_disposition = 'next_cycle' and billed_invoice_id = null. It's queued.
Then, when the customer's cycle invoice generates, PortaPro runs sweepJobFeesToInvoice:
That's the loop. Idempotent (a fee can never bill twice because the candidate predicate is billed_invoice_id IS NULL), self-healing (deleting the invoice re-queues its swept fees automatically via ON DELETE SET NULL), and compensating (if the stamp step fails, the copies are deleted so there's no half-swept state).
Why this used to be broken (R3 fix)
Pre-July-2026, sweepJobFeesToInvoice was fully written and tested — but had zero callers. Neither cycle-invoice generator called it. So every "the $150 emergency fee lands on next cycle" promise was structurally broken. The fees stayed queued forever; only a manual sweep or a human noticing would surface them. The R3 fix wired the sweep into both generateCycleInvoice and generateRateCardInvoice. Now it runs on every cycle generation.
Also fixed at the same time: the cycle-invoice generator returned subtotal + feeTotal where feeTotal was never declared — throwing a ReferenceError *after* writing the invoice + items + tax to the database. Operators saw a failure toast on a successful write, and no fees swept anyway. Both parts fixed in R3.
The review flag
When a cycle invoice sweeps fees, PortaPro stamps a job_fees_swept review flag on the invoice. The pre-close review panel shows a "Job fees swept: $X across N jobs" chip so operators can verify before sending.
---
EMR-5: Immediate-Billing Fees
If a fee's disposition is immediate, PortaPro doesn't queue it — the operator's next action is usually Create Invoice for the job. The fee flows into that invoice as a line, tax pipeline applies, done.
Use immediate for:
If you attached a fee as immediate and haven't billed yet, you can change it to next_cycle on the fee's edit action (as long as it's still unbilled).
---
EMR-6: Damage, Dry-Run, and After-Hours Fees
These aren't a separate feature — they're just common surcharge catalog entries:
Damage fee — customer damaged the unit (drilled holes in the door, dropped it, spray-painted it). Attach at your standard damage rate, next_cycle for cycle customers so it lands on their monthly invoice. Photo documentation goes on the maintenance report for the pickup/service that surfaced the damage; the fee references the job.
Dry-run fee — driver arrived, couldn't service (locked gate, container moved, wrong address). Attach a dry-run fee to the skipped job. For cycle customers on non-term work, the fee sweeps into their cycle invoice (this is the R6 unification described in the Roll-Off Swap Cycle article, which extended the sweep source filter to include dry_run_fee).
After-hours / weekend / holiday fee — attach at emergency-call time. If your ops has different rates for weeknight-after-6pm vs weekend vs holiday, keep three catalog entries; PortaPro doesn't auto-detect the time-of-call.
---
EMR-7: Unit Replacement in the Field
When a driver arrives and finds the unit destroyed (fire, storm, vandalism, drilling), the workflow is:
- From the driver app or dispatch: open the unit's detail (or the site's inventory) and set the tracked item's status to retired with a reason
- The tracked item flips out of the active pool
- Any equipment_assignments on the unit close out
- Add a fresh tracked item at the same location (map pin) — either an existing yard unit or a new inventory item
- The Rate Card row keeps its declared count; the replacement fills the physical placement
- Damage fee if customer-caused
- Emergency replacement fee if you charged premium urgency
- Fees flow through the normal disposition path (next_cycle for cycle customers, immediate for others)
The service_location_recurring_units declaration doesn't change — the customer is still contractually renting 3 standard units at the site. Only the physical placement (tracked item id) changes.
The retire lifecycle
Retired units are excluded from every active calculation:
Retiring is soft — the row stays for history. If a mistake was made and the unit isn't actually retired, an admin can flip status back to available from the inventory tool.
---
EMR-8: On-Call Customer Setup
Some customers are on-call: "call us when you need us." They don't have a scheduled cycle. Setup:
On the customer:
On the site (if any):
For each call:
---
EMR-9: How Emergency Fees Interact with Autopay
If the customer has autopay enabled and the fee sweeps onto their cycle invoice, the cycle invoice charges via Stripe on send — no operator action needed. The customer's card takes the full amount including the swept emergency fee.
If the customer is on autopay but the fee is immediate (one-off invoice), autopay does not charge one-off invoices automatically by default — the operator sends and the customer pays via the emailed link, or via manual Charge on File from the invoice detail. If you want one-off invoices to auto-charge for autopay customers, that's a per-invoice choice, not global.
---
EMR-10: Priority Auto-Derivation (Note)
There is no current setting to auto-derive priority from a fee type (e.g. "any job with an After-Hours fee is automatically priority"). Priority is set per-job manually. This is noted in the July 2026 audit as an enhancement, not a bug — the intentional design is that priority is a scheduling signal owned by dispatch, and pricing is a separate axis owned by billing. If your ops wants them linked, do it via checklist ("when you attach the emergency fee, flip the priority toggle") rather than automation.
---
EMR-11: Troubleshooting
"I attached a $150 emergency fee three weeks ago, it's on the job, but the customer's cycle invoice doesn't include it."
Pre-July-2026 (R3 fix): sweepJobFeesToInvoice had zero callers, so no fees ever swept. Update to the latest deploy. Post-fix, verify:
If all three are true and the fee still doesn't sweep, delete the cycle invoice (which unstamps any partial sweep via the ON DELETE SET NULL cascade) and regenerate.
"The invoice says '$0.00 subtotal' after generation but there was a fee attached."
Pre-July-2026, the generator threw a ReferenceError after successfully writing the invoice and items — the operator saw a "failed" toast on a successful write. Post-fix, this doesn't happen. If it's happening on a post-July-2026 build, that's a bug worth reporting.
"A fee shows up twice on the same cycle invoice."
The sweep is idempotent — a fee with billed_invoice_id set is excluded from candidacy. If you're seeing duplicates, the previous stamp may have been cleared manually. Verify the fee's audit history in the surcharge_applications table.
"The job is marked Priority but the driver's route plan didn't put it first."
Priority is a sort signal, not a hard constraint. If earlier jobs on the route are pinned to specific times (customer arrival windows, geographic proximity), the planner may leave them in place. To force sequencing, drag-reorder in the route plan editor.
"I retired a tracked item but it's still showing on the site inventory tab."
Cache invalidation is per-query — retiring dispatches a refetch but if the tab was open, refresh it. If the unit persists after refresh, the retire didn't stamp — check the item's actual status column; if it's not retired, the operation failed silently. Try again from the inventory admin view.
"The dry-run fee I attached to a skipped job isn't on the customer's cycle invoice."
For roll-off term customers, dry-run fees route to the final statement — see Roll-Off Swap Cycle, section ROL-8.
"The Priority toggle is missing on a completed job."
Priority is editable while the job is active; once the job is completed or cancelled, the priority flag is frozen for history. To change historical priority for reporting, an admin can update via the database directly — but this is rare.
"I attached an emergency fee to the wrong job."
Open the fee's row in the Fees section of the current (wrong) job, click Delete. Then re-attach on the correct job. If the wrong job's cycle invoice already generated with the fee swept: delete that invoice (which un-sweeps the fee via the compensating logic), re-attach correctly, regenerate.
---
