RCM Pipeline Unified Contract
8-stage RCM lifecycle, canonical types, shared mock encounter pool, dual data-source mapping, export matrix, 11 invariants.
Master reference for the end-to-end Revenue Cycle Management pipeline. Every surface — Cashier, Medical Coder, Revenue Collection, FPA Export — imports shared types from a single source of truth to guarantee type-level alignment.
Status: Active — updated 2026-05-27 Related docs:
- NHSO Revenue Engine Master Plan — 10-phase charge-to-settlement engine
- Billing Queue Auto-Sync —
trg_sync_billing_queuetrigger pattern - FPA Data Warehouse — Star schema for hospital FP&A
- Medical Coder Remaining Backend Work — Backend deployment checklist
1. Pipeline Overview
┌─────────┐ ┌──────────┐ ┌────────────┐ ┌──────────┐ ┌───────────┐ ┌──────────────┐ ┌─────────┐
│ CHECKOUT │───▶│ CODING │───▶│ VALIDATION │───▶│ CLAIM │───▶│ INVOICE │───▶│ REP RESULT │───▶│ SETTLED │
│ (Cashier)│ │ (Coder) │ │ (Rules) │ │ BATCH │ │ (Export) │ │ (Reconcile) │ │ (AR) │
└─────────┘ └──────────┘ └────────────┘ └──────────┘ └───────────┘ └──────────────┘ └─────────┘
│ │ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼ ▼
Cashier UI Medical Coder Coder + Rules Revenue Revenue Revenue Revenue
(billing) Landing + Engine (edge Collection Collection Settlement Settlement
Worklist function) DataGrid Export Wizard Command Center AR Close
7 Pipeline Stages
| # | Stage | Thai | Owner Surface | Entry Condition | Exit Condition |
|---|---|---|---|---|---|
| 0 | CHECKOUT |
ชำระเงิน | Cashier | Patient visit complete | Payment confirmed OR billing finalized |
| 1 | CODING |
ลงรหัส | Medical Coder | OPD: paid/settled; IPD: discharged | All ICD assigned, coding finalized |
| 2 | VALIDATION |
ตรวจสอบ | Medical Coder + Rules | Coding complete | No BLOCK alerts (WARN OK) |
| 3 | CLAIM_BATCH |
จัดชุด | Revenue Collection | Validation passed | Batch confirmed, encounters grouped |
| 4 | INVOICE |
ใบแจ้งหนี้ | Revenue Collection Export | Batch created | Invoice exported to payor |
| 5 | REP_RESULT |
ผล REP | Revenue Settlement | Invoice submitted | REP response parsed (D-case/P-case/Paid) |
| 6 | AR_SETTLEMENT |
ตั้งลูกหนี้ | Revenue Settlement | REP received | Full payment / write-off / appeal closed |
| 7 | SETTLED |
ปิดบัญชี | Revenue Settlement | AR resolved | — |
Stage Colors (consistent across all UI surfaces)
CHECKOUT: #64748b (slate)
CODING: #8b5cf6 (purple)
VALIDATION: #f97316 (orange)
CLAIM_BATCH: #17b8e0 (teal)
INVOICE: #185FA5 (blue)
REP_RESULT: #0F6E56 (green-dark)
AR_SETTLEMENT: #dc2626 (red)
SETTLED: #22c55e (green)
2. Canonical Type Definitions
Source of Truth File
web/src/containers/medical-coder/components/RevenuePipelineContract.ts
All surfaces MUST import pipeline types from this file. Do NOT redefine PipelineStage,
CodingStatus, CodingOutput, or EncounterPipelineState elsewhere.
2.1 Pipeline Stage
export const PIPELINE_STAGES = [
'CODING', 'VALIDATION', 'CLAIM_BATCH', 'INVOICE',
'REP_RESULT', 'AR_SETTLEMENT', 'SETTLED',
] as const;
export type PipelineStage = (typeof PIPELINE_STAGES)[number];
2.2 Coding Status (7 states)
┌──────────────────────────────────────┐
│ AWAITING │ ← Entry: encounter queued
└──────────┬───────────────────────────┘
│ coder picks up
▼
┌──────────────────────────────────────┐
│ IN_PROGRESS │
└──────────┬──────────┬────────────────┘
│ │ request review
│ ▼
│ ┌──────────────────┐
│ │ REVIEW_REQUESTED │──▶ REVIEW_COMPLETED ──▶ back to IN_PROGRESS
│ └──────────────────┘
│ submit
▼
┌──────────────────────────────────────┐
│ PENDING_VALIDATION │
└──────────┬──────────┬────────────────┘
│ │ rules fire BLOCKs
│ ▼
│ ┌──────────────────┐
│ │ VALIDATION_FAILED │──▶ coder fixes ──▶ PENDING_VALIDATION
│ └──────────────────┘
│ all clear
▼
┌──────────────────────────────────────┐
│ COMPLETED │ ← Exit: ready for claim batch
└──────────────────────────────────────┘
export type CodingStatus =
| 'AWAITING' | 'IN_PROGRESS' | 'COMPLETED'
| 'REVIEW_REQUESTED' | 'REVIEW_COMPLETED'
| 'VALIDATION_FAILED' | 'PENDING_VALIDATION';
2.3 Claim Status (6 states — Revenue Collection surface)
Prepare ──▶ Pending ──▶ Verified ──▶ Exported
│
┌─────┴─────┐
▼ ▼
Mismatch Flagged
export type ClaimStatus =
| 'Prepare' | 'Pending' | 'Verified'
| 'Exported' | 'Mismatch' | 'Flagged';
2.4 Payment Status (4 states)
export type PaymentStatus = 'Draft' | 'Pending' | 'Paid' | 'Settled';
2.5 CodingOutput — What the Coder Produces
export interface CodingOutput {
encounterId: string;
encounterNo: string; // VN for OPD, AN for IPD
patientHn: string;
patientName: string;
encounterClass: 'AMB' | 'IMP';
schemeCode: string;
schemeName: string;
codingStatus: CodingStatus;
validationStatus: 'PASSED' | 'WARNED' | 'BLOCKED' | null;
principalDiagnosisCode: string; // ICD-10 PDx
principalDiagnosisDesc: string;
secondaryDiagnosisCodes: string[];
procedureCodes: string[]; // ICD-9-CM
drgCode: string;
drgDescription: string;
rw: number; // Relative Weight
adjRw: number; // Adjusted RW
estimatedPayment: number; // ฿ estimated
qualityScore: number; // 0–100
blockCount: number;
warnCount: number;
coderId: string;
coderName: string;
codedAt: string; // ISO 8601
finalizedAt: string | null;
}
2.6 ReceivableRow — Revenue Collection Grid Row
export interface ReceivableRow {
id: string; // UUID (Supabase) or ObjectId (MongoDB)
_id: string; // Always present for dual-source compat
encounterClass: 'OPD' | 'IPD' | 'ER';
hn: string;
patientName: string;
patientTitle: string;
vn: string;
an?: string;
schemeName: string;
schemeCode: string;
schemeColor: string;
debtorGroup: string;
totalAmount: number; // ฿ total charge
claimAmount: number; // ฿ claimable portion
reimAmount: number; // ฿ reimbursement expected
nonReimAmount: number; // ฿ non-reimbursable
paidAmount: number; // ฿ received
coverage: number; // ฿ coverage ceiling
visitDate: string;
dischargeDate?: string;
diagnosis: string;
clinicName: string;
doctorName: string;
hospitalName: string;
claimStatus: ClaimStatus;
anomalies: AnomalyFlag[];
verifiedBy?: string;
verifiedAt?: string;
// Raw refs for drill-through
patient: any;
encounter: any;
payor: any;
payorPlan: any;
}
2.7 AnomalyFlag — Quality Flags on Receivables
export interface AnomalyFlag {
type: 'same-day-duplicate' | 'non-reim-spike' | 'missing-icd'
| 'high-value' | 'mismatch' | 'expired-scheme';
severity: 'warning' | 'error';
message: string;
}
2.8 EncounterPipelineState — Per-Encounter Journey Tracker
export interface EncounterPipelineState {
encounterId: string;
encounterNo: string;
patientHn: string;
patientName: string;
encounterClass: 'AMB' | 'IMP';
schemeCode: string;
currentStage: PipelineStage;
stageHistory: { stage: PipelineStage; enteredAt: string; exitedAt: string | null }[];
coding: CodingOutput | null;
claimBatchId: string | null;
invoiceDocNo: string | null;
repCode: string | null;
arItemId: string | null;
totalCharge: number;
claimedAmount: number;
approvedAmount: number;
paidAmount: number;
variance: number;
hasAlert: boolean;
alertCount: number;
}
3. Data Source Architecture
Dual Data Source (MongoDB + Supabase)
┌───────────────────────────────────────────────────────────────┐
│ WRITE PATH (Backend) │
│ NestJS Services ──▶ MongoDB (canonical truth) │
│ financial.SalesOrder, financial.Invoice, clinical.Encounter │
└──────────────────────────────┬────────────────────────────────┘
│ Moleculer events
▼
┌───────────────────────────────────────────────────────────────┐
│ EVENT BUS (NATS) │
│ financial.salesOrder.paid, clinical.encounter.discharged │
└──────────────────────────────┬────────────────────────────────┘
│ Edge functions subscribe
▼
┌───────────────────────────────────────────────────────────────┐
│ READ MODEL (Supabase) │
│ encounter_journey_cache ← orchestrator │
│ coding_worklist ← fn_auto_populate_coding_worklist trigger │
│ department_queues ← queue placement triggers │
│ billing_queue ← trg_sync_billing_queue trigger │
└──────────────────────────────┬────────────────────────────────┘
│ Realtime subscriptions
▼
┌───────────────────────────────────────────────────────────────┐
│ FRONTEND SURFACES │
│ Medical Coder ← coding_worklist + encounter_journey_cache │
│ Revenue Collection ← receivables (derived from journey cache) │
│ FPA Dashboard ← aggregated views │
└───────────────────────────────────────────────────────────────┘
Field Name Mapping (Critical for Dual-Source)
| Concept | MongoDB/REST | Supabase | SharedMockEncounter |
|---|---|---|---|
| Record ID | _id (ObjectId) |
id (UUID) |
id + encounterId |
| Encounter ref | encounter |
encounter_id |
encounterId |
| Patient ref | patient |
patient_id |
patientId |
| Patient HN | patient.hn |
patient_hn |
hn |
| Patient name | patient.firstName + lastName |
patient_name |
patientFullName |
| Visit number | encounter.vn |
encounter_vn |
vn |
| Coding status | codingStatus |
coding_status |
codingStatus |
| Claim status | claimStatus |
claim_status |
claimStatus |
| Created at | createdAt |
created_at |
(derived) |
Live Read Path + Mock Fallback (Revenue Collection grid)
The Receivables / Revenue Collection grid reads through one hook with a graceful fallback:
useReceivablesUnified (web/packages/rcm-kit/src/financial/hooks/useReceivablesUnified.ts)
├─ live: SELECT * FROM v_receivables_unified (Supabase, realtime, HTTP 200)
│ → maps snake_case view cols → ReceivableRow + 4 derived cols
│ (currentStage, daysInAr, paymentModel, denial*)
└─ mock: shared-encounter-pool (when no client / view 404 / view empty)
- The hook is client-agnostic (rcm-kit stays Supabase-free): the app/sandbox passes its
own Supabase client; rcm-kit only needs
.from().select()+.channel(). - When live rows are present, the sandbox’s
deriveStageFromClaimStatus/deriveArDays/derivePaymentModelprefer the view’scurrent_stage/days_in_ar/payment_modeland only fall back to the hash-mocks when Supabase is unreachable. See §6.4.
When Supabase read models return empty (no data deployed), surfaces fall back to the Shared Mock Encounter Pool — a deterministic dataset generated from a seeded PRNG.
web/packages/rcm-kit/src/financial/mock-data/shared-encounter-pool.ts
The pool generates 80 encounters with consistent IDs across all consumers. Same encounter ID appears in Medical Coder worklist AND Revenue Collection grid.
4. Surface-to-Surface Data Flow
4.1 Cashier → Medical Coder
Trigger: OPD payment confirmed OR IPD discharge finalized
Cashier (financial service)
└─▶ SalesOrder.status = 'Paid'
└─▶ hospital_events: financial.salesOrder.paid
└─▶ encounter-orchestrator edge fn
└─▶ encounter_journey_cache.financial_summary = { status: 'Paid' }
└─▶ fn_auto_populate_coding_worklist trigger
└─▶ coding_worklist row inserted (coding_status = 'AWAITING')
└─▶ Medical Coder realtime subscription fires
Key contract: coding_worklist row must carry:
encounter_id,patient_id,encounter_class(from journey cache)scheme_code,clinic_id,clinic_name(from clinical_context JSONB)patient_hn,patient_name(denormalized for display)discharge_date(from admission_context or financial_summary timestamp)coding_status = 'AWAITING',priority(URGENT for EMER, ROUTINE otherwise)
4.2 Medical Coder → Revenue Collection
Trigger: Coder finalizes all codes, validation passes
Medical Coder (coding service)
└─▶ coding_worklist.coding_status = 'COMPLETED'
└─▶ coding_worklist.validation_status = 'PASSED' or 'WARNED'
└─▶ CodingOutput produced:
{ encounterId, pdxCode, drgCode, rw, adjRw, estimatedPayment, qualityScore }
└─▶ Revenue Collection reads encounter as ReceivableRow
claimStatus = 'Prepare' (ready for batching)
Key contract: Revenue Collection needs from coding:
diagnosis(PDx display text) for anomaly detectionestimatedPaymentflows intoreimAmountqualityScore/blockCount/warnCountinform anomaly flagsclaimStatusstarts atPreparewhen coding completes
4.3 Revenue Collection → FPA Export
Trigger: User selects encounters, opens Export Wizard
Revenue Collection DataGrid
└─▶ User selects rows (checkboxes) or "Select All"
└─▶ "Export…" button opens ExportWizardDialog
└─▶ Step 1: Scope (date range, include mode, hospital)
└─▶ Step 2: Scheme selection (UC/SSS/CSMBS/LGO/MVA per OPD/IPD)
└─▶ Step 3: Preflight validation
├─ Missing ICD → error (blocks export)
├─ Same-day duplicate → warning
├─ High-value claim → warning
└─ Non-reim spike → warning
└─▶ Step 4: Format selection
├─ UC → NHSO 16 flat files
├─ SSS → SSDATA format
├─ CSMBS → Eclaim 2021 XML
└─ Custom → CSV/Excel
└─▶ Step 5: Generate + download
└─▶ ClaimInvoice created (status = 'Exported')
└─▶ ReceivableRow.claimStatus = 'Exported'
Key contract: ExportWizardDialog receives:
interface ExportWizardDialogProps {
open: boolean;
onClose: () => void;
rows: ReceivableRow[]; // Full dataset (for scheme grouping)
selectedIds: string[]; // Pre-selected row IDs (from DataGrid)
onExportSubmit: (scope: ExportScope, format: string) => Promise<void>;
}
4.4 FPA Export → Revenue Reports
Trigger: Aggregation over claim invoices
ClaimInvoice records (exported + settled)
└─▶ FPA Data Warehouse views (materialized)
├─▶ Monthly revenue by department
├─▶ Payor mix analysis
├─▶ AR aging buckets (0-30, 31-60, 61-90, 91-120, 121-180, 180+)
├─▶ Claim settlement timelines
└─▶ Daily/weekly/monthly trends
└─▶ Revenue Reports page
└─▶ KpiSummary, DepartmentRevenue, PayorMixEntry, ArAgingBucket
5. Shared Mock Encounter Pool Contract
Location
web/packages/rcm-kit/src/financial/mock-data/shared-encounter-pool.ts
Design Principles
- Deterministic — seeded PRNG (
mulberry32(20260527)), same seed = same data - Single pool — 80 encounters, each carrying fields for ALL consumers
- Transform functions — each surface gets its own projection of the same data
- Singleton —
getSharedMockPool()returns memoized array
SharedMockEncounter Interface
interface SharedMockEncounter {
// ── Identity ──
id: string; // UUID-like deterministic ID
encounterId: string; // Same as id
patientId: string; // Deterministic patient ref
// ── Patient demographics ──
hn: string; // HN 0000001–0000080
patientTitle: string; // นาย/นาง/น.ส./ด.ช./ด.ญ.
patientFirstName: string; // Thai first name
patientLastName: string; // Thai last name
patientFullName: string; // title + first + last
patientSex: 'M' | 'F';
patientDob: string; // ISO date
patientIdCard: string; // 13-digit Thai ID
// ── Visit info ──
vn: string; // VN000000001–VN000000080
an: string | undefined; // AN only for IPD
encounterClass: 'OPD' | 'IPD';
visitDate: string; // ISO date
dischargeDate: string | undefined;
los: number; // Length of stay (IPD only)
clinic: { _id: string; code: string; name: string };
doctorName: string;
// ── Coding ──
codingStatus: CodingStatus;
coderName: string;
codingStartedAt: string | undefined;
codingCompletedAt: string | undefined;
agingDays: number;
icd10: string[]; // ICD-10 codes assigned
icd9: string[]; // ICD-9-CM procedure codes
pdxCode: string; // Principal diagnosis
pdxDisplay: string;
drg: { code: string; name: string; rw: number };
qualityScore: number;
validationStatus: 'PASSED' | 'WARNED' | 'BLOCKED' | null;
blockCount: number;
warnCount: number;
// ── Financial ──
paymentStatus: PaymentStatus;
payor: { _id: string; code: string; name: string };
payorPlan: { _id: string; code: string; name: string; payorId: string };
totalAmount: number; // ฿ total charge
claimAmount: number; // ฿ claimable
paidAmount: number;
coverage: number;
estimatedPayment: number;
debtorGroup: string;
// ── Revenue Collection ──
claimStatus: ClaimStatus;
mainHospital: { _id: string; code: string; name: string };
}
Transform Functions
| Function | Consumer | Output Type |
|---|---|---|
getSharedMockPool() |
All | SharedMockEncounter[] (80 rows, singleton) |
toCodingWorklistRows(pool?) |
Medical Coder worklist | TCodingWorklistRow[] |
toCodingDashboardSummary(encounterClass?) |
Medical Coder KPIs | TCodingDashboardSummary[] |
toRevenueCollectionRows(pool?) |
Revenue Collection grid | (legacy row shape)[] |
getMockCodingCounts(encounterClass?) |
Medical Coder + Revenue Collection banner | Record |
getClaimReadyEncounters() |
Export Wizard | SharedMockEncounter[] (COMPLETED only) |
SCHEME_COLORS |
All surfaces | Record<string, string> |
Coding Status Distribution (weighted)
| Status | Weight | ~Count (of 80) |
|---|---|---|
| AWAITING | 25% | ~20 |
| IN_PROGRESS | 15% | ~12 |
| COMPLETED | 30% | ~24 |
| REVIEW_REQUESTED | 5% | ~4 |
| VALIDATION_FAILED | 8% | ~6 |
| PENDING_VALIDATION | 17% | ~14 |
Scheme Distribution
UC: #e65100 (deep orange) ~25% of encounters
SSS: #1565c0 (blue) ~20%
CSMBS:#2e7d32 (green) ~15%
LGO: #9c27b0 (purple) ~8%
MVA: #d32f2f (red) ~7%
INS: #00838f (teal) ~10%
SELF: #455a64 (grey) ~8%
AIA: #f9a825 (amber) ~7%
6. Supabase Read Model Tables
6.1 coding_worklist (migration 042)
Auto-populated by fn_auto_populate_coding_worklist trigger on encounter_journey_cache.
| Column | Type | Source |
|---|---|---|
id |
UUID | PK |
encounter_id |
TEXT | journey cache |
patient_id |
TEXT | journey cache |
encounter_class |
TEXT | AMB/IMP/EMER |
scheme_code |
TEXT | clinical_context.payor.code |
clinic_id |
TEXT | clinical_context.clinic._id |
clinic_name |
TEXT | clinical_context.clinic.name |
patient_hn |
TEXT | denormalized |
patient_name |
TEXT | denormalized |
discharge_date |
TIMESTAMPTZ | admission or financial timestamp |
coding_status |
TEXT | starts AWAITING |
coder_id |
TEXT | assigned coder |
coder_name |
TEXT | assigned coder name |
coding_started_at |
TIMESTAMPTZ | when coder picks up |
coding_completed_at |
TIMESTAMPTZ | when finalized |
pdx_code |
TEXT | ICD-10 principal |
pdx_display |
TEXT | PDx display text |
drg_estimate |
TEXT | DRG code |
rw_estimate |
NUMERIC | Relative Weight |
adjrw_estimate |
NUMERIC | Adjusted RW |
estimated_payment |
NUMERIC | ฿ estimated |
validation_status |
TEXT | PASSED/WARNED/BLOCKED/null |
block_count |
INT | validation block count |
warn_count |
INT | validation warn count |
info_count |
INT | validation info count |
quality_score |
NUMERIC | 0–100 |
aging_days |
INT | days since discharge |
priority |
TEXT | ROUTINE/URGENT/STAT |
6.2 coding_connectors (migration 041)
Pluggable routing registry for coding capabilities.
| Column | Type | Purpose |
|---|---|---|
id |
UUID | PK |
module |
TEXT | always ‘medical-coder’ |
capability |
TEXT | validate/estimate-drg/quality-score/suggest-icd/… |
provider_type |
TEXT | edge_function/dify_workflow/n8n_webhook/external_api |
endpoint_url |
TEXT | webhook/API URL |
priority |
INT | dispatch order (1 = primary) |
is_active |
BOOLEAN | enabled/disabled |
scheme_filter |
TEXT[] | optional scheme whitelist |
encounter_class_filter |
TEXT[] | optional OPD/IPD whitelist |
RPC: fn_resolve_coding_connectors(capability, hospital, scheme, enc_class)
6.3 encounter_journey_cache (migration 003+)
Central read model for patient journey. RCM-relevant JSONB columns:
| JSONB Column | RCM-Relevant Fields |
|---|---|
financial_summary |
status, totalAmount, claimAmount, paidAmount, updatedAt |
clinical_context |
clinic, payor, payorPlan, doctor |
admission_context |
dischargeDate, admissionDate, los |
coding_summary |
codingStatus, pdxCode, drgCode, rw, qualityScore |
6.4 v_receivables_unified (migration 20260528s) — the canonical grid read model
Applied to demo Supabase hynsmfrevlsegbmjnoiy on 2026-05-28. One row per billed
encounter; the single query the Receivables grid runs. It is a VIEW (always live), not a
table — current_stage is derived, never stored.
v_receivables_unified ◀── encounter_journey_cache (financial_summary, encounter_header, …)
◀── coding_worklist (CODING / VALIDATION)
◀── claim_batch_line + claim_batch (CLAIM_BATCH / INVOICE / AR / SETTLED)
◀── remittance (REP_RESULT, reconciliation)
◀── encounter_payment_binding + contract (payment_model, scheme)
◀── denial_code_master (denial reason, appealability)
Derived columns added on top of ReceivableRow: current_stage (rcm_pipeline_stage
enum — 7 stages, see note below), days_in_ar, payment_model, denial_code /
denial_reason / denial_is_appealable, plus claim_batch_id / remittance_id for nav.
Stage derivation walks downstream→upstream so the most-advanced state wins (remittance RECONCILED → SETTLED; batch SETTLED/PARTIALLY_SETTLED → AR_SETTLEMENT; remittance present → REP_RESULT; batch SUBMITTED/ACK/PROCESSING/ADJUDICATED → INVOICE; batch line exists → CLAIM_BATCH; coding_worklist validation_status set → VALIDATION; else CODING).
7-stage resolution (closes contradiction #2 in billing-rcm-unified-master §7.2): the shipped enum
rcm_pipeline_stageisCODING → VALIDATION → CLAIM_BATCH → INVOICE → REP_RESULT → AR_SETTLEMENT → SETTLED. CHECKOUT is NOT a stage — it is the entry condition into CODING. FrontendPIPELINE_STAGES(7) and this DB enum (7) now agree.
Prerequisite migrations (apply in order; all idempotent): 20260528f (contract +
encounter_payment_binding) → 20260528p (claim_batch / claim_batch_line / remittance /
denial_code_master) → 055 (adds patient_display_name to journey cache) → 20260528s.
Companion summary views (same migration): v_receivables_pipeline_distribution (KPI
mini-bar), v_receivables_ar_buckets (4 aging buckets), v_receivables_denial_by_scheme.
Audit table: rcm_stage_transitions (RLS-enabled) — reserved for the future
rcm.pipeline.advance write command (not yet built).
Known data-mapping gaps (cosmetic, not wiring): amounts read financial_summary → total_amount/claim_amount/reim_amount; this demo’s financial_summary uses total/netPay
(billing-queue trigger keys), so ฿ show 0 until a key-mapping pass. Patient name sourced from
patient_display_name; blank where encounter_header lacks a name key.
Read by the useReceivablesUnified hook (§3). Canonical SQL:
infrastructure/medbase/migrations/20260528s_receivables_unified_view.sql.
7. Cross-Surface Navigation
Navigation Links (bi-directional)
| From | To | Route | Trigger |
|---|---|---|---|
| Medical Coder Landing | Revenue Collection | /revenue-collection |
HubCard “Revenue Collection” |
| Medical Coder Worklist | Encounter Workspace | /medical-coder/encounter/:id |
Row action button (OpenInNew icon) |
| Encounter Workspace | Revenue Settlement CC | /revenue-settlement-command-center?stage=VALIDATION&from=coder |
“Finalize” button (seeds mock pipeline + navigates) |
| Revenue Settlement CC | Stage-specific tab | /revenue-settlement?tab=N |
PipelineStageIndicator stage click |
| Revenue Collection | Medical Coder Landing | /medical-coder |
Chip “Medical Coder” in banner |
| Revenue Collection | Revenue Reports | /revenue-reports |
Chip “Reports” in banner |
| Revenue Collection | FPA Dashboard | /fpa-dashboard |
Chip “FP&A” in banner |
| Revenue Settlement | Revenue Collection | /revenue-collection |
Back nav |
Shared Banner Contract
Revenue Collection displays a shared-data banner:
SHARED MOCK DATA — {total} encounters linked to Medical Coder
({completed} coded, {awaiting + in_progress} pending)
[Medical Coder →]
Medical Coder displays in Quick Jumps:
Revenue Collection — {completed} claim-ready encounters
8. Export Format Matrix
| Scheme | Format | File Structure | Generator |
|---|---|---|---|
| UC (บัตรทอง) | NHSO 16-file | ADP/ADT/CHA/CHT/DIA/DRG/INS/IPD/IRF/LVD/ODX/OOP/OPD/ORF/PAT/PRO | nhso-16-file-generator (planned) |
| SSS (ประกันสังคม) | SSDATA | Single CSV per batch | ssdata-exporter (planned) |
| CSMBS (ข้าราชการ) | Eclaim 2021 | XML per invoice | eclaim-xml-builder (planned) |
| LGO (อปท.) | NHSO 16-file variant | Same as UC with LGO headers | Shares UC generator |
| MVA (พ.ร.บ.) | Custom PDF + CSV | Per-accident claim form | mva-claim-builder (planned) |
| INS / AIA (ประกันเอกชน) | Fax claim form | PDF per claim | insurance-claim-pdf (planned) |
| SELF (จ่ายเอง) | Receipt only | No export needed | — |
9. Anomaly Detection Rules
Applied in Revenue Collection when building ReceivableRow.anomalies:
| Rule | Type | Severity | Condition |
|---|---|---|---|
| Same-day duplicate | same-day-duplicate |
warning | Same HN + same visitDate, different encounter |
| Missing ICD | missing-icd |
error | diagnosis empty or ‘-’ |
| High-value claim | high-value |
warning | claimAmount > ฿30,000 |
| Non-reim spike | non-reim-spike |
warning | nonReimAmount > ฿2,000 |
| Scheme-claim mismatch | mismatch |
error | Claim exceeds scheme ceiling (planned) |
| Expired scheme | expired-scheme |
error | Payor plan expired (planned) |
10. Implementation Checklist
Currently Wired (2026-05-27)
- [x]
RevenuePipelineContract.ts— canonical types (7 stages, CodingOutput, EncounterPipelineState) - [x]
shared-encounter-pool.ts— 80 deterministic mock encounters - [x] Medical Coder landing mock fallback (when Supabase empty)
- [x] Revenue Collection using shared pool data
- [x] Cross-navigation: Medical Coder ↔ Revenue Collection ↔ Reports ↔ FP&A
- [x] ReceivablesDataGrid with anomaly detection + Pipeline stage column
- [x] ExportWizardDialog (mock export flow)
- [x]
coding_worklistSupabase trigger (migration 042) - [x]
coding_connectorsregistry (migration 041) - [x]
PipelineStageIndicatorcomponent — visual 7-stage stepper with click-to-navigate - [x]
CoderWorkstation— split-pane encounter workspace (discharge summary + coding ladder + DRG preview) - [x]
CdiQueryCard— CDI query drawer for clinician reviews - [x] Encounter page data extraction (ICD-10/ICD-9 from live encounter → CoderWorkstation props)
- [x] Revenue Settlement Command Center — PipelineStageIndicator in header, auto-pipeline seeding from coder
- [x] Revenue Collection — pipeline tracker banner with stage indicator + Reports/FP&A nav chips
- [x] Worklist → Workspace flow — row action navigates to
/medical-coder/encounter/:id - [x] Finalize → Settlement flow — seeds mockPipelineStore + navigates to command center
- [x] Sandbox targets:
CodingWorklist(worklist→workspace),CoderWorkstation,RevenueCollection - [x]
v_receivables_unifiedview (migration20260528s) applied to demo Supabase 2026-05-28 — derivescurrent_stagefrom real downstream tables (see §6.4); prereqs20260528f+20260528p+055applied - [x]
useReceivablesUnifiedhook (rcm-kit) — live view read + realtime + mock fallback;ReceivablesGridTargetreads it (badge flips live/mock), grid live at 91 rows - [x] Fixed 6 latent bugs in
20260528s(4 column refs, denial join, ORDER-BY alias) — the view had never been run before
Backend Wiring Needed
- [ ] Deploy
coding_worklist+coding_connectorsmigrations to PH demo Supabase - [ ] Wire
fn_auto_populate_coding_worklisttrigger (currently defined but tables may not exist) - [ ] Backend service:
coding.serviceMoleculer actions (getCodingWorklist, updateCodingStatus, submitValidation) - [ ] NATS events:
coding.encounter.assigned,coding.encounter.completed,coding.validation.passed - [ ] Edge function:
encounter-orchestratorhandler for coding_summary projection
Frontend Wiring Needed
- [x] Revenue Collection: switch from mock to Supabase when data available (keep mock fallback) — done via
useReceivablesUnified+v_receivables_unified(2026-05-28) - [ ] Medical Coder worklist: switch
listAllActiveEncounterREST to Supabasecoding_worklistrealtime - [x] Pipeline stage indicator on Revenue Collection rows (Pipeline column in ReceivablesDataGrid)
- [x] Pipeline stage indicator on Revenue Collection page (tracker banner above grid)
- [x] Pipeline stage indicator on Revenue Settlement Command Center header
- [ ] Revenue Reports: wire to FPA data warehouse views (currently mock data)
- [ ] FPA Dashboard: connect to materialized views
Export Pipeline Needed
- [ ] NHSO 16-file generator (Phase 4 of NHSO master plan)
- [ ] SSDATA format exporter
- [ ] Eclaim 2021 XML builder
- [ ] Pre-export validation engine (not just anomaly flags)
- [ ] Batch management UI (group encounters into submission batches)
11. Backend Service Architecture
NestJS Modules (Financial Service)
| Module | Location | Purpose |
|---|---|---|
rcmValidation |
services/financial/.../modules/rcmValidation/ |
Thin proxy to Deno BRE edge function; validates encounter financial context |
claimInvoice |
services/financial/.../modules/claimInvoice/ |
Claim batching, export generation, invoice CRUD |
settlement |
services/financial/.../modules/settlement/ |
AR settlement workflow |
reconciliation |
services/financial/.../modules/reconciliation/ |
Payment reconciliation |
eclaimConnector |
services/financial/.../modules/eclaimConnector/ |
E-claim submission to NHSO |
nhsoAutoTagger |
services/financial/.../modules/nhsoAutoTagger/ |
Auto-tagging engine wrapper |
revenueCollection |
services/financial/.../modules/revenueCollection/ |
Revenue grid & dashboard |
orCaseCosting |
services/financial/.../modules/orCaseCosting/ |
OR case-level cost tracking |
Coder Management (Administration Service)
| Module | Location | Purpose |
|---|---|---|
coder |
services/administration/.../modules/coder/ |
Coder entity CRUD, status transitions, concurrent-coder soft-lock |
Edge Functions (Deno)
| Function | Location | Purpose |
|---|---|---|
coding-rules-engine |
infrastructure/medbase/functions/coding-rules-engine/ |
Validation dispatcher with country packs (TH_NHSO, CSMBS, commercial, generic) |
rcm-rule-engine |
infrastructure/medbase/functions/rcm-rule-engine/ |
Business rules engine (late penalties, quotas, surcharges) |
eclaim-connector |
infrastructure/medbase/functions/eclaim-connector/ |
E-claim submission, REP response parsing |
coding-ai-assistant |
infrastructure/medbase/functions/coding-ai-assistant/ |
Dify workflow integration for AI suggestions |
Backend ↔ Frontend Enum Alignment
The backend CoderStatus enum uses Thai-legacy string values (matching the older API). The frontend
CodingStatus uses uppercase constants. Both represent the same 7 states:
Backend (CoderStatus) |
Frontend (CodingStatus) |
Thai Label |
|---|---|---|
'Await Register' |
'AWAITING' |
รอลงรหัส |
'In Progress' |
'IN_PROGRESS' |
กำลังลงรหัส |
'Await Reconsider' |
'REVIEW_REQUESTED' |
รอแพทย์ทบทวน |
'Acknowledged' |
'REVIEW_COMPLETED' |
แพทย์ทบทวนแล้ว |
'Pending Validation' |
'PENDING_VALIDATION' |
รอตรวจสอบ |
'Validation Failed' |
'VALIDATION_FAILED' |
ตรวจสอบไม่ผ่าน |
'Registed' |
'COMPLETED' |
ให้รหัสแล้ว |
Important: The backend uses 'Registed' (not ‘Registered’) — this is a legacy typo preserved for
backward compatibility. The Supabase read model coding_worklist uses the frontend-style uppercase
constants. The fn_auto_populate_coding_worklist trigger maps backend values to frontend constants.
Backend Pipeline Stages
The backend does not export an explicit PIPELINE_STAGES constant. Instead, stage transitions are
event-driven via NATS:
financial.salesOrder.paid → triggers CODING stage (coding_worklist insertion)
coding.encounter.completed → triggers VALIDATION (coding-rules-engine edge fn)
coding.validation.passed → encounter eligible for CLAIM_BATCH
claimInvoice.batch.confirmed → INVOICE stage
eclaim.submission.complete → REP_RESULT stage
eclaim.rep.response.received → AR_SETTLEMENT stage
settlement.ar.resolved → SETTLED stage
The canonical PipelineStage type lives in the frontend (RevenuePipelineContract.ts) and is the
single source of truth. Backend services don’t need the enum because they operate on individual
entity status transitions (SalesOrder.status, Coder.status, ClaimInvoice.status) rather than a
unified pipeline state.
12. Invariants
- One encounter = one pipeline journey — an encounter cannot be in two stages simultaneously
- Stage ordering is strict — CODING → VALIDATION → CLAIM_BATCH → INVOICE → REP_RESULT → AR_SETTLEMENT → SETTLED (no skipping)
- BLOCK validation = hard stop — encounters with
validationStatus = 'BLOCKED'cannot advance past VALIDATION - Deterministic mock data — same seed produces identical encounters; changing the seed breaks cross-surface alignment
- Dual data source safety — always support both
_idandid,encounterandencounter_id(see §3) - Mock fallback — when Supabase returns zero rows, surfaces MUST fall back to shared mock pool (never show empty state)
- Scheme colors are canonical — all surfaces import
SCHEME_COLORSfrom shared pool, never redefine - Pipeline types are canonical — all surfaces import from
RevenuePipelineContract.ts, never redefine