Manual + Automated QA Plan — Catalog + Pricing RCM foundation
Companion to docs/plans/2026-06-27-feat-catalog-pricing-rcm-foundation-plan.md. Executed per phase (strangler). This is a money-critical, max-blast-radius change — the gating principle is golden price-parity: nothing that resolves money ships until the resolver provably returns the same amount the legacy columns did.
Pre-flight (before any phase)
- Prerequisites merged: #1753 (care-package) and #1470 (per-doctor pricing); #1734 closed;
structure.sqlregenerated on a clean DB and loads viadb:test:prepare. - Full backend suite green on the merged base (baseline).
- Spike confirmed:
ALTER TABLE … SET SCHEMAcarries (or migration recreates) RLS policies + FKs + partial-unique indexes + thepublic_read/vendor tenant-NULL escape.
Phase A — catalog domain + package re-home (no behavior change)
Automated
- Full backend suite green after the move (every
Identity::ServicePackage|PackageComponent|PatientPackageSubscriptionref →Catalog::*). - Marketplace #1373 request/e2e specs green (browse → order → verify/webhook → subscription).
- Care-package #1086 specs green (associate → invoice paid → activation; initiate → webhook → activation).
- NOBYPASSRLS canaries:
catalog.service_packagespublic_read(active clinic package readable with no GUC), vendor tenant-NULL escape intact, subscription tenant-isolation intact. -
structure.sqlloads;bin/rails routesclean;tsc -b+ Biome green onpatient/+frontend/.
Manual (staging)
- CMS: list / create / edit / associate a service package — unchanged behavior.
- Patient app marketplace: browse vendor packages, buy one (Cashfree sandbox) → subscription active.
- Care package: clinic associate → collect payment → activates; patient-app purchase (if Phase 4 resumed) → activates.
Phase B — payer-aware prices + PriceResolver (the money-critical phase)
Golden price-parity (the ship gate) — for EACH of the 7 call sites, assert PriceResolver.resolve(...) == legacy column across the matrix:
- consultation (in_clinic) — per-doctor
consultation_fee - consultation (telemedicine) —
telemedicine_fees - facility service — facility default + #1392 per-doctor override (fallback chain)
- facility procedure — price + per-doctor override
- care package —
effective_price(with/without discount) - marketplace package —
effective_price(commission base unchanged) - invoice line / patient service — resolved == previously-passed
unit_price -
PriceNotFoundraised (never silent 0) where no price exists.
Dual-read transition
- Resolver runs alongside legacy reads; drift is logged/alerted; zero drift over a soak window before old columns are dropped.
- Typesense
consultation_feeindex +max_feefilter read the resolver; doctor-search fee filter returns identical results.
RLS / payer
-
catalog.pricestenant-isolation (FORCE) verified with/without ambient tenant. -
payer_id IS NULL(self-pay) resolves; a seeded non-NULL payer rate resolves ahead of self-pay (fallback order correct).
Manual (staging)
- Book an appointment (in-clinic + telemedicine) → invoice amount unchanged vs. pre-migration.
- Bill a clinical service/procedure for a doctor with a per-doctor override → charged amount unchanged.
- Buy a care package + a marketplace package → amounts + activation unchanged.
Phase C — catalog_items Charge Master + gap fill
-
catalog_itemsregistry created;prices.chargeablere-points at catalog_items with parity (no amount changes). - IPD bed/room categories + lab/pharmacy modeled as priced
catalog_items; resolver returns their rates. - Reference-code links resolve (catalog_item → clinical/reference code) for claims-readiness.
Phase D (future — not this initiative)
IPD charge capture, claims/pre-auth/TPA, denials, per-payer rate-card population — separate QA plans.
Rollback
- Each phase is a discrete migration + dual-read; revert = restore old reads (columns retained until parity proven) +
dbdown-migration. No phase drops a price column until its golden-parity gate passes.