feat(govfee): auto-quote + collect state fees for at-cost trucking services

At-cost services (IRP/IFTA/intrastate) only collected our service fee at
checkout; the variable state fee was never billed, so orders stalled at
authorization_signed and the filing card would have had to front large IRP fees.

New end-to-end, hands-off flow (you only approve the final filing):
  1. After authorization is signed, state_trucking auto-estimates the gov fee
     from intake (base/op states, power units, weight) via gov_fee.estimate_gov_fee.
  2. Creates a CHILD compliance order (CG-..., service_fee=0, gov_fee=estimate,
     parent_order_number set, migration 099) that flows through the EXISTING
     checkout/payment/webhook machinery.
  3. Emails the customer a payment link to /order/pay (new self-contained page)
     showing every method with correct surcharges — ACH 0% (Stripe 0.8%/ cap
     absorbed, no GoCardless needed), card/PayPal 3%, Klarna 6%, crypto 0%.
  4. Order holds at awaiting_government_fee_approval until paid.
  5. On payment, handlePaymentComplete detects the child (parent_order_number)
     and re-dispatches the PARENT with gov_fee_paid=true, which proceeds to
     prepare + queue the filing and stops at ready_to_file for your approval.

IRP fees are estimates billed at cost (refund overage / rebill shortfall); IFTA
decals + most intrastate fees are near-exact. Tunable via env.
This commit is contained in:
justin 2026-06-16 04:35:45 -05:00
parent 3e13b722f6
commit 861f2fbfd4
5 changed files with 579 additions and 0 deletions

View file

@ -0,0 +1,20 @@
-- 099: Government-fee child orders for at-cost compliance services.
--
-- At-cost services (IRP, IFTA, intrastate authority, etc.) collect only our
-- SERVICE fee at checkout; the actual government/state fee is variable and
-- "billed at cost" afterward. To collect it we create a CHILD compliance_orders
-- row (service_fee_cents = 0, gov_fee_cents = the quoted state fee) that flows
-- through the EXISTING checkout/payment-picker/webhook machinery unchanged, and
-- email the customer a payment link with every payment method + correct
-- surcharges. parent_order_number links that child back to the original order so
-- the worker can resume filing once the fee is paid.
--
-- Idempotent.
ALTER TABLE compliance_orders
ADD COLUMN IF NOT EXISTS parent_order_number text;
-- Look up a parent's gov-fee children quickly (and vice-versa).
CREATE INDEX IF NOT EXISTS idx_compliance_orders_parent
ON compliance_orders (parent_order_number)
WHERE parent_order_number IS NOT NULL;

View file

@ -1770,6 +1770,39 @@ export async function handlePaymentComplete(
// reporting year. Non-fatal — the customer still sees ingestion
// counts even without a grant.
if (order_type === "compliance" || order_type === "compliance_batch") {
// ── Government-fee child order paid → resume the parent's filing ──────
// At-cost services (IRP/IFTA/intrastate) bill the state fee via a child
// order (parent_order_number set). When that child is paid, re-dispatch the
// PARENT to the worker with gov_fee_paid=true so it proceeds to file. The
// child itself needs none of the normal compliance post-processing.
const parentNo = (order.parent_order_number as string) || "";
if (parentNo) {
try {
const { rows: prows } = await pool.query(
"SELECT service_slug FROM compliance_orders WHERE order_number = $1",
[parentNo],
);
const parentSlug = prows[0]?.service_slug || "";
const workerUrl = process.env.WORKER_URL || "http://workers:8090";
await fetch(`${workerUrl}/jobs`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
action: "process_compliance_service",
order_name: parentNo,
order_number: parentNo,
service_slug: parentSlug,
client_approved: true, // authorization already signed earlier
gov_fee_paid: true,
}),
});
console.log(`[checkout] Gov-fee ${order_id} paid → re-dispatched parent ${parentNo} to file`);
} catch (e) {
console.error(`[checkout] Failed to resume parent after gov-fee ${order_id}:`, e);
}
return; // child order needs no further compliance processing
}
try {
await grantCDRStudyAccess(order, order_id);
} catch (grantErr) {