Kulcare Docs

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.sql regenerated on a clean DB and loads via db:test:prepare.
  • Full backend suite green on the merged base (baseline).
  • Spike confirmed: ALTER TABLE … SET SCHEMA carries (or migration recreates) RLS policies + FKs + partial-unique indexes + the public_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|PatientPackageSubscription ref → 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_packages public_read (active clinic package readable with no GUC), vendor tenant-NULL escape intact, subscription tenant-isolation intact.
  • structure.sql loads; bin/rails routes clean; tsc -b + Biome green on patient/ + 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
  • PriceNotFound raised (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_fee index + max_fee filter read the resolver; doctor-search fee filter returns identical results.

RLS / payer

  • catalog.prices tenant-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_items registry created; prices.chargeable re-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) + db down-migration. No phase drops a price column until its golden-parity gate passes.

On this page