Skip to content

Payments, Checkout, Coupons & Upsells

Who this is for: support staff (billing questions), PMs, finance-adjacent staff, new hires.

  1. When a patient reaches the payment step of their case, the price shown is always recalculated on the server from the case’s configured price for that service/state — the system never trusts a price sent from the patient’s browser.
  2. The patient pays by credit/debit card through the platform’s payment processor.
  3. Successful payment marks the case as paid and fires a “case paid” event, which is what allows the case to move forward (booking, intake, etc.) and is also the trigger for referral rewards (see Referrals).

Three checkout page designs, one shared backend

Section titled “Three checkout page designs, one shared backend”

The patient portal actually has three different-looking checkout page implementations (a standard one, an alternate-design one, and an FMLA-specific one), plus matching pairs for the pre-payment summary and payment-processing screens. Which one a patient sees depends on which onboarding funnel they came from and, for FMLA, the service configuration’s checkout setting — but all three call the exact same underlying pricing/payment logic, so behavior (and any pricing bug fix) is consistent across all three regardless of which one is shown.

A note on the care portal’s own payment webhook

Section titled “A note on the care portal’s own payment webhook”

The care portal has its own copy of a Stripe payment webhook, but it is currently disabled (commented out) in the codebase — the live, active Stripe webhook that actually processes payment confirmations today lives in the patient portal. This is worth knowing if you’re troubleshooting a payment that didn’t register: check the patient portal’s webhook logs, not the care portal’s.

  • Coupons can be a fixed dollar amount off or a percentage off, and are scoped to a specific brand.
  • Coupons can optionally expire or be turned inactive.
  • A discount can never make the price go below zero.
  • If a coupon (or a free promotional service) brings the price all the way to zero, checkout skips the payment processor entirely and the case is marked paid directly — but it still fires the same “case paid” event as a real charge, so downstream steps behave the same either way.
  • Some services can offer the patient an additional related service at specific moments in their journey — before starting intake, after finishing intake, or on a dedicated add-on checkout screen.
  • Whether upsell offers are shown at all, and at which of those moments, is a configurable setting per brand.
  • If a patient buys an add-on service, it is created as its own case, automatically linked to the same doctor as the original case.

For each paid case, the system tracks an internal breakdown of the payment: a support-operations share, an estimated payment-processing fee, the doctor’s payout, and the remaining company revenue share. This breakdown is for internal financial tracking — it is not shown to the patient.

  • Whether there are usage caps (e.g. “one use per patient”) enforced on coupon codes was not confirmed in the code reviewed — flagged as a real risk area worth double-checking with the team, since a reusable 100%-off coupon combined with the “skip payment processor” free-checkout path could be used repeatedly.
  • parkingmd-patient-portal/src/trpc/routers/checkout/ (index.ts, helpers.ts)
  • parkingmd-patient-portal/src/models/coupons.ts
  • parkingmd-patient-portal/src/lib/upsell.ts
  • parkingmd-patient-portal/documentation/service-flow.md (section 5 — addon services and upsells)
  • parkingmd-care-portal/docs/upsell-care-portal-blueprint.md
  • parkingmd-care-portal/src/lib/stripe.ts (refund handling)
  • parkingmd-patient-portal/documentation/additional-documents-plan.md (historical planning note only — used solely for the risk flag above, not as fact about current behavior)
  • parkingmd-patient-portal/src/app/task/[taskid]/checkout/, checkout-flow/, checkout-new/ (three page implementations sharing trpc/routers/checkout/)
  • parkingmd-patient-portal/src/components/*/CheckoutCard.tsx (picks checkout vs checkout-new based on serviceConfigurations.checkoutUrl)
  • parkingmd-care-portal/src/app/api/webhooks/stripe/route.ts (disabled/commented out) vs. parkingmd-patient-portal/src/app/api/webhooks/stripe/route.ts (the live one)