NHSO Revenue Engine Master Plan
10-phase plan for the NHSO billing layers: charge masters, TMT seeding, F9 auto-tagging, 16-file generator, revenue optimization.
Goal: Build the missing charge-mapping, auto-tagging, 16-file generation, and revenue optimization layers so medOS can (a) generate NHSO-compliant claim files from first principles, (b) auto-tag every encounter for maximum reimbursement, and © surface missed-revenue opportunities to coders and finance staff.
Table of Contents
- Current State Audit
- Architecture Decisions
- Phase 0 — Charge Item Master Tables
- Phase 1 — Terminology Seeding (TMT / TMLT / ADP)
- Phase 2 — Auto-Tagging Engine
- Phase 3 — 16-File Generator
- Phase 4 — HIPData / Authen Code Integration
- Phase 5 — Revenue Optimization Engine
- Phase 6 — Charge-Level Reconciliation
- Phase 7 — Appeal & Write-Off Workflow UI
- Phase 8 — Fee Schedule & Project Code Engine
- Phase 9 — Revenue-at-Risk Dashboard
- Dependency Graph
- Estimated Scope
Current State Audit
What’s Built (✅)
| Component | Location | Status |
|---|---|---|
| RCM BRE (rule engine) | medbase/functions/rcm-rule-engine/ |
✅ Production — evaluates 15+ rule types per th-nhso-uc.json |
| Coding validation engine | medbase/functions/coding-rules-engine/ |
✅ Production — DRG v5, CC/MCC capture monitoring |
| e-Claim connector | medbase/functions/eclaim-connector/ |
✅ Production — submit, REP import, govt sync, statement import |
| RCM persistence tables | medbase/migrations/010_rcm_tables.sql |
✅ Production — alert, batch, rep_response, correction, denial queue |
| Terminology cache | medbase/migrations/012_terminology_cache.sql |
✅ Production — supports TMT/TMLT/ICD/LOINC/SNOMED/ATC/NHSO_CATEGORY |
| Terminology server | medbase/functions/terminology-server/ |
✅ Production — cache lookup, $translate, ValueSet expansion |
| FP&A warehouse | medbase/migrations/090-092 |
✅ Production — fact_encounter, fact_charge, dim_department, dim_diagnosis, dim_procedure |
| Gold dim_scheme | medbase/migrations/013_gold_layer.sql |
✅ Production — NHSO_UC base rate 8350 THB/adjRW |
| Revenue settlement UI | rcm-kit/financial/components/revenue-settlement/ |
✅ 30+ components — dashboard, batch, REP, correction, denial panels |
| NestJS service proxies | services/financial/src/.../rcmValidation/, eclaimConnector/ |
✅ Thin proxy to Deno edge functions |
| Airflow DAG | infrastructure/airflow/dags/medos_eclaim_submission.py |
✅ Daily batch orchestration |
What’s Missing (❌)
| Gap | Impact | Phase |
|---|---|---|
| No ADP master table — internal charges can’t map to NHSO ADP Type + Code | Can’t generate ADP file or tag claims with project codes | 0, 1 |
| No ChrgItem mapping — local charge categories don’t map to NHSO 19-category system | Can’t generate CHA/CHT files | 0 |
TMT/TMLT seeds empty — terminology_cache table exists but has zero TMT/TMLT rows |
Drug/lab lines missing national codes on claims | 1 |
| No auto-tagging engine — HOSxP’s F9-equivalent doesn’t exist | Every claim requires manual code entry = missed money | 2 |
No 16-file generator — th-nhso-eclaim.json declares nhso_16files format but no code generates them |
Can’t submit to hospitals still using file-based e-Claim | 3 |
| No HIPData integration — no authen code verification or real-time scheme check | Authen code field exists but is never validated | 4 |
| No revenue suggestions — coding engine validates but doesn’t suggest better codes | Leave 5-15% adjRW on the table | 5 |
| No per-charge reconciliation — statement totals tracked, not per-line approved/denied | Can’t investigate underpayments at charge level | 6 |
| Appeal/write-off UI incomplete — tables exist, no frontend workflow | D/P cases go to queue but staff can’t act on them | 7 |
| No fee schedule engine — NHSO’s 24-item prevention/promotion list not prompted | Eligible services not tagged → not billed | 8 |
| No revenue-at-risk dashboard — no aggregate view of money being left on the table | Finance can’t prioritize where to focus | 9 |
Architecture Decisions
AD-1: Config-driven JSON, not hardcoded TypeScript
All country-specific logic lives in JSON files loaded at runtime by Deno edge functions. This is the established pattern (th-nhso-uc.json, th-nhso-eclaim.json, th-nhso-coding.json). New engines follow the same pattern — zero country logic in TypeScript.
AD-2: Supabase tables for editable masters, JSON for immutable rules
- Charge item mappings → Supabase tables (
nhso_adp_master,nhso_chrgitem_map, etc.) because finance staff need to edit them in-app. - Structural rules (late penalty tiers, diagnostic pathways, quota limits) → JSON country packs because they change only at fiscal year boundaries.
AD-3: Auto-tagger is a Deno edge function
The auto-tagger runs as medbase/functions/nhso-auto-tagger/ — same hosting model as BRE and e-Claim connector. NestJS proxies through the financial service. Frontend calls at two points:
- Pre-billing — when a cashier opens the billing screen, auto-tagger suggests tags.
- Pre-submission — when a batch is assembled, auto-tagger validates all encounters have required tags.
AD-4: 16-file generator extends the existing eclaim-connector
The generator lives in medbase/functions/eclaim-connector/generators/nhso-16files.ts — called by the existing submitClaims() when exportFormat === 'nhso_16files'. No new edge function needed.
AD-5: Revenue optimization is advisory, never auto-mutating
The optimization engine suggests — it never auto-changes a code. All suggestions go to rcm_ai_suggestion_log (existing table) with action_type: 'coding_suggestion'. Coders accept/reject in the UI.
Phase 0 — Charge Item Master Tables
Goal: Create the foundational mapping tables that everything else depends on.
Migration: 100_nhso_charge_masters.sql
┌──────────────────────────────────────────────────────────────────────────┐
│ nhso_adp_master │
│ ───────────────────── │
│ id (serial PK) │
│ adp_type smallint NOT NULL -- 3=service, 4=PP, 5=project_code │
│ adp_code text NOT NULL -- 'WALKIN', 'UCEP24', '36598' │
│ adp_name_th text NOT NULL │
│ adp_name_en text │
│ scheme_codes text[] DEFAULT '{NHSO_UC}' -- which schemes use it │
│ unit_price numeric(12,2) -- 0 for project codes, fee for services│
│ claim_track text -- 'op_anywhere', 'ucep', 'er_ext', etc. │
│ effective_from date NOT NULL │
│ effective_to date │
│ is_active boolean DEFAULT true │
│ UNIQUE(adp_type, adp_code) │
└──────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────┐
│ nhso_chrgitem_map │
│ ───────────────────── │
│ id (serial PK) │
│ local_charge_category text NOT NULL -- medOS charge_category enum │
│ nhso_category_code text NOT NULL -- NHSO ChrgItem code (1-19) │
│ nhso_category_name_th text NOT NULL │
│ nhso_category_name_en text │
│ charge_category_detail text -- sub-category for ORF file │
│ UNIQUE(local_charge_category, nhso_category_code) │
└──────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────┐
│ nhso_fee_schedule │
│ ───────────────────── │
│ id (serial PK) │
│ fee_code text NOT NULL UNIQUE │
│ fee_name_th text NOT NULL │
│ fee_name_en text │
│ unit_price numeric(12,2) NOT NULL │
│ service_type text -- 'prevention', 'promotion', 'screening' │
│ eligible_departments text[] -- dept codes where this auto-triggers │
│ auto_trigger_conditions jsonb -- age/gender/frequency rules │
│ fiscal_year text NOT NULL DEFAULT '2568' │
│ effective_from date NOT NULL │
│ effective_to date │
│ is_active boolean DEFAULT true │
└──────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────┐
│ nhso_project_code_rule │
│ ───────────────────── │
│ id (serial PK) │
│ project_code text NOT NULL -- ADP Code (e.g. 'WALKIN') │
│ adp_type smallint NOT NULL DEFAULT 5 │
│ rule_name_th text NOT NULL │
│ rule_name_en text │
│ trigger_conditions jsonb NOT NULL │
│ -- { │
│ -- "encounter_class": ["AMB"], │
│ -- "scheme_code": ["NHSO_UC"], │
│ -- "visit_type": ["walk_in"], │
│ -- "hospmain_match": false, │
│ -- "department_types": ["er"], │
│ -- "time_window": { "start": "16:30", "end": "08:29" }, │
│ -- "icd_prefixes": ["Z51.5"], │
│ -- "min_activity_codes": 1 │
│ -- } │
│ priority smallint NOT NULL DEFAULT 100 -- lower = higher pri │
│ auto_inject boolean DEFAULT true -- inject into billing line │
│ requires_confirmation boolean DEFAULT false │
│ effective_from date NOT NULL │
│ effective_to date │
│ is_active boolean DEFAULT true │
│ UNIQUE(project_code, effective_from) │
└──────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────┐
│ nhso_ucef_marker │
│ ───────────────────── │
│ id (serial PK) │
│ charge_item_code text NOT NULL -- internal product/item code │
│ ucef_eligible boolean NOT NULL DEFAULT false │
│ ucef_code text -- UCEP-specific override code if different │
│ notes text │
│ UNIQUE(charge_item_code) │
└──────────────────────────────────────────────────────────────────────────┘
Admin UI: /admin/nhso-masters
Four tabs in the existing admin-kit:
- ADP Master — CRUD for
nhso_adp_master. Import from CSV (HOSxPnhso_adp_type+nhso_adp_codetables). - Charge Category Map — CRUD for
nhso_chrgitem_map. Maps medOS 14-category → NHSO 19-category. - Fee Schedule — CRUD for
nhso_fee_schedule. 24-item NHSO PP fee list per fiscal year. - Project Code Rules — CRUD for
nhso_project_code_rule. Visual rule builder for auto-injection conditions.
Seed Data
Market pack infrastructure/market-packs/medos-thailand/:
seed-nhso-adp-master.sql— ~200 rows (3 ADP types × ~60 codes each)seed-nhso-chrgitem-map.sql— 19 rows mapping medOS categories → NHSO categoriesseed-nhso-fee-schedule.sql— 24 rows (FY2568 prevention/promotion items)seed-nhso-project-code-rules.sql— ~15 rules (WALKIN, UCEP24, ER-EXT, ER-QUAL, OP-ANYWHERE variants, CANCER, HOMEWARD, IMC, PALLIATIVE, etc.)
Dependencies
- None — this is the foundation layer.
Deliverables
- [ ] Migration
100_nhso_charge_masters.sql - [ ] Seed files in
market-packs/medos-thailand/ - [ ] Admin UI at
/admin/nhso-masters(4 tabs) - [ ] RLS policies + realtime subscriptions
Phase 1 — Terminology Seeding (TMT / TMLT / ADP)
Goal: Populate terminology_cache with Thai national code systems so every drug and lab line carries the required national code.
TMT (Thai Medicines Terminology)
Source: GPO TMT database (~30,000 codes) or NLEMOnline.
Target: terminology_cache rows with system = 'TMT'.
// Example terminology_cache row for TMT
{
"system": "TMT",
"code": "1000088",
"display": "Metformin 500mg tablet",
"display_th": "เมทฟอร์มิน 500 มก. เม็ด",
"properties": {
"isActive": true,
"inNLEM": true,
"nlemCategory": "บัญชี ก",
"genericName": "metformin",
"atcCode": "A10BA02",
"dosageForm": "tablet",
"strength": "500 mg",
"tradeName": "Glucophage"
},
"translations": {
"ATC": "A10BA02",
"SNOMED": "109071002"
}
}
TMLT (Thai Medical Laboratory Terminology)
Source: ~5,000 codes from the Thai Lab Standard Terminology project.
Target: terminology_cache rows with system = 'TMLT'.
{
"system": "TMLT",
"code": "L0001",
"display": "Complete Blood Count (CBC)",
"display_th": "ตรวจนับเม็ดเลือดทั้งหมด",
"properties": {
"isActive": true,
"specimen": "blood",
"method": "automated",
"loincMapping": "57021-8"
},
"translations": {
"LOINC": "57021-8"
}
}
NHSO_CATEGORY (ChrgItem)
Source: NHSO 19-category billing classification.
Target: terminology_cache rows with system = 'NHSO_CATEGORY'.
Concept Maps (Cross-System Links)
terminology_concept_map entries:
LOCAL_DRUG-to-TMT— maps internal drug product codes → TMT codesLOCAL_LAB-to-TMLT— maps internal lab item codes → TMLT codesLOCAL_CHARGE-to-NHSO_CATEGORY— maps charge items → NHSO ChrgItem codesTMT-to-ATC— TMT → ATC cross-reference
Implementation
- Seed script:
infrastructure/market-packs/medos-thailand/seed-terminology-tmt.ts— TypeScript script that loads the TMT CSV and bulk-upserts intoterminology_cache. - Seed script:
infrastructure/market-packs/medos-thailand/seed-terminology-tmlt.ts— same pattern for TMLT. - Seed script:
infrastructure/market-packs/medos-thailand/seed-nhso-categories.sql— 19 rows. - Concept map admin UI — extend the existing terminology server admin to show
terminology_concept_maprows and allow manual review/correction. - Drug master integration — when a drug product is created/edited in the medication setup UI, the TMT code field auto-suggests from
terminology_cache WHERE system='TMT'with fuzzy search ondisplay_th. - Lab master integration — when a lab item is created/edited, TMLT auto-suggest from
terminology_cache WHERE system='TMLT'.
Dependencies
- Phase 0 (for NHSO_CATEGORY seeding consistency)
terminology_cachetable (already exists)terminology_concept_maptable (already exists)
Deliverables
- [ ] TMT seed script + CSV source data
- [ ] TMLT seed script + CSV source data
- [ ] NHSO category seed SQL
- [ ] Concept map seed SQL (LOCAL_DRUG→TMT, LOCAL_LAB→TMLT)
- [ ] Drug master TMT auto-suggest integration
- [ ] Lab master TMLT auto-suggest integration
Phase 2 — Auto-Tagging Engine
Goal: Replicate HOSxP’s F9 auto-injection behavior — when a billing screen opens (or a batch is assembled), auto-tag every encounter with the correct ADP codes, project codes, and fee schedule items.
Edge Function: medbase/functions/nhso-auto-tagger/
Architecture:
┌─────────────────────────────────────────────────────────────┐
│ nhso-auto-tagger (Deno Edge Function) │
│ │
│ Input: EncounterBillingContext (encounter + charges) │
│ Output: AutoTagResult (suggested tags + injections) │
│ │
│ 1. Load encounter context (scheme, class, dept, time) │
│ 2. Load all active project-code rules from Supabase │
│ 3. Evaluate each rule's trigger_conditions against context │
│ 4. For matched rules: generate billing line injections │
│ 5. Load fee-schedule items eligible for this encounter │
│ 6. Cross-check existing charges for missing national codes │
│ 7. Return: { projectCodes[], feeScheduleItems[], │
│ missingTmtCodes[], missingTmltCodes[], │
│ adpMappings[], warnings[] } │
└─────────────────────────────────────────────────────────────┘
Key Types:
interface EncounterBillingContext {
encounterId: string;
encounterClass: 'AMB' | 'IMP' | 'EMER';
schemeCode: string;
patientCitizenId?: string;
hospmain?: string; // patient's registered primary hospital
hospsub?: string; // patient's registered sub-unit
treatingHcode: string; // this hospital's HCODE
departmentCode: string;
departmentType: string;
serviceTime: string; // ISO datetime — for time-window rules
visitType?: string; // walk_in, appointment, referral, emergency
admissionDate?: string;
dischargeDate?: string;
diagnoses: Array<{ code: string; type: 'principal' | 'secondary' }>;
procedures: Array<{ code: string; name?: string }>;
charges: Array<{
chargeItemCode: string;
chargeCategory: string;
quantity: number;
unitPrice: number;
tmtCode?: string; // may already be mapped
tmltCode?: string; // may already be mapped
adpType?: number; // may already be tagged
adpCode?: string; // may already be tagged
}>;
referralInfo?: {
fromHcode: string;
referralType: string;
sameProvince: boolean;
};
ipdInfo?: {
ipdType?: string;
accidentAeType?: string;
};
}
interface AutoTagResult {
encounterId: string;
schemeCode: string;
// Project codes to inject as zero-priced billing lines
projectCodes: Array<{
adpType: number;
adpCode: string;
nameTh: string;
nameEn: string;
autoInject: boolean;
requiresConfirmation: boolean;
matchedRule: string; // rule ID for audit
}>;
// Fee schedule items the encounter is eligible for
feeScheduleItems: Array<{
feeCode: string;
nameTh: string;
unitPrice: number;
alreadyCharged: boolean; // true if a matching charge line already exists
}>;
// Charges missing national codes
missingTmtCodes: Array<{
chargeItemCode: string;
chargeDescription: string;
suggestedTmt?: string; // from concept map if available
confidence?: number;
}>;
missingTmltCodes: Array<{
chargeItemCode: string;
chargeDescription: string;
suggestedTmlt?: string;
confidence?: number;
}>;
// ADP type/code mappings for charges
adpMappings: Array<{
chargeItemCode: string;
suggestedAdpType: number;
suggestedAdpCode: string;
source: 'rule' | 'category_map' | 'manual';
}>;
// Claim track classification
claimTrack: {
track: 'in_network' | 'op_anywhere_same_province' | 'op_anywhere_cross_province' |
'ucep' | 'ucep24' | 'er_ext' | 'er_quality' | 'referral' | 'standard';
autoDetected: boolean;
hospmainMatch: boolean; // treatingHcode === hospmain?
provinceMatch: boolean;
};
warnings: string[];
evaluatedAt: string;
}
Project Code Rule Evaluation Logic
For each active rule in nhso_project_code_rule (sorted by priority):
1. Check encounter_class matches
2. Check scheme_code matches
3. Check visit_type matches (if specified)
4. Check hospmain_match:
- false: treatingHcode !== patient's hospmain (cross-facility)
- true: treatingHcode === patient's hospmain (home facility)
5. Check department_types (if specified)
6. Check time_window (if specified — for ER off-hours rules)
7. Check icd_prefixes (if specified — for palliative, cancer, etc.)
8. Check min_activity_codes (if specified — for palliative: 5 required)
9. If ALL conditions match → add to projectCodes[]
Claim Track Auto-Detection
Given: treatingHcode, hospmain, hospsub, encounterClass, visitType, referralInfo
if encounterClass === 'EMER' && visitType === 'emergency':
if first24Hours: claimTrack = 'ucep24'
else: claimTrack = 'ucep'
elif treatingHcode === hospmain:
if isOffHours(serviceTime) && departmentType === 'er':
claimTrack = 'er_ext' or 'er_quality'
else:
claimTrack = 'standard' (home facility, normal hours)
elif sameProvince(treatingHcode, hospmain):
claimTrack = 'op_anywhere_same_province'
else:
claimTrack = 'op_anywhere_cross_province'
NestJS Proxy
services/financial/src/api/financial/modules/nhsoAutoTagger/:
nhsoAutoTagger.service.ts— thin proxy to Deno edge functionnhsoAutoTagger.controller.mixin.ts— REST endpointPOST /api/v2/financial/auto-tag
Frontend Integration Points
-
Cashier billing screen — when opening a billing encounter, call auto-tagger. Display suggested project codes as checkboxes (pre-checked if
autoInject: true). Display fee schedule items as a “Did you bill these?” checklist. Highlight charges missing TMT/TMLT. -
Batch creation — in
BatchCreationPanel.tsx, run auto-tagger on all encounters in the batch. Show summary: “42 encounters tagged, 3 need manual review, 5 missing TMT codes.” -
Pre-submission validation — the existing BRE (
rcm-rule-engine) gains a new evaluator that checks auto-tagger results and blocks encounters missing required tags.
Dependencies
- Phase 0 (nhso_project_code_rule, nhso_fee_schedule, nhso_chrgitem_map tables)
- Phase 1 (terminology_cache populated with TMT/TMLT for code suggestions)
Deliverables
- [ ] Edge function
medbase/functions/nhso-auto-tagger/(index.ts, types.ts) - [ ] NestJS proxy
services/financial/.../nhsoAutoTagger/ - [ ] Frontend auto-tag panel in cashier billing screen
- [ ] Batch-level auto-tag summary in
BatchCreationPanel.tsx - [ ] New BRE evaluator:
autoTagCompleteness
Phase 3 — 16-File Generator
Goal: Generate the NHSO standard 16 flat files (ADP, ADT, CHA, CHT, DIA, DRG, INS, IPD, IRF, LVD, ODX, OOP, OPD, ORF, PAT, PRO) from encounter + charge data.
Generator Module: medbase/functions/eclaim-connector/generators/nhso-16files.ts
Architecture:
eclaim-connector/index.ts
└─ submitClaims(req, config)
└─ if req.exportFormat === 'nhso_16files':
└─ generate16Files(encounters, config)
├─ generatePAT(encounters) → PAT.txt
├─ generateOPD(encounters) → OPD.txt (AMB encounters)
├─ generateIPD(encounters) → IPD.txt (IMP encounters)
├─ generateDIA(encounters) → DIA.txt (all diagnoses)
├─ generatePRO(encounters) → PRO.txt (all procedures)
├─ generateDRG(encounters) → DRG.txt (DRG codes)
├─ generateCHA(encounters) → CHA.txt (charge totals)
├─ generateCHT(encounters) → CHT.txt (charge details)
├─ generateADP(encounters) → ADP.txt (additional payment)
├─ generateINS(encounters) → INS.txt (insurance/scheme)
├─ generateADT(encounters) → ADT.txt (admit/discharge/transfer)
├─ generateIRF(encounters) → IRF.txt (IP referral)
├─ generateORF(encounters) → ORF.txt (OP referral)
├─ generateODX(encounters) → ODX.txt (OP diagnosis detail)
├─ generateOOP(encounters) → OOP.txt (OP procedure detail)
└─ generateLVD(encounters) → LVD.txt (leave day)
→ zip all → return as base64 or upload to Supabase storage
File Format Spec
Each file is pipe-delimited (|), one row per encounter (or per line item for detail files). Character encoding: UTF-8 (with optional TIS-620 export for legacy systems via exportEncoding in connector config).
Column definitions stored in: medbase/functions/eclaim-connector/generators/nhso-16files-schema.json
This schema file defines for each of the 16 files:
- Column names
- Data types
- Max lengths
- Required/optional
- Source mapping (which field from
Encounter16FileContextpopulates it) - Default values
Example — ADP file columns:
AN|DATEOPD|TYPE|CODE|QTY|RATE|TOTAL|PERSON_ID|SEQ|CAGCODE|DOSE|CA_TYPE|SERIALNO|TOTCOPAY
Extended Encounter Context
The 16-file generator needs more data than the current ClaimEncounterPayload. New type:
interface Encounter16FileContext extends ClaimEncounterPayload {
// PAT file
citizenId: string;
patientTitle: string;
firstName: string;
lastName: string;
dateOfBirth: string;
gender: 'M' | 'F';
marriageStatus: string;
nationality: string;
address: { addrpart: string; moopart: string; tmbpart: string; amppart: string; chwpart: string };
// INS file
inscl: string; // insurance type code
subtype: string; // sub-insurance type
hospmain: string; // main registered hospital
hospsub: string; // sub-registered hospital
// Charge details (for CHA/CHT/ADP files)
chargeLines: Array<{
chargeDate: string;
chargeCategory: string; // medOS category
nhsoCategoryCode: string; // mapped NHSO ChrgItem code
chargeCode: string;
description: string;
quantity: number;
unitPrice: number;
totalAmount: number;
adpType?: number;
adpCode?: string;
tmtCode?: string;
tmltCode?: string;
personId?: string; // CID of person charge is for
}>;
// ADT file (IP only)
admissionType?: string;
admissionWeight?: number;
dischargeStatus?: string;
dischargeType?: string;
// Leave days (LVD file)
leaveDays?: Array<{ leaveDate: string; returnDate: string; hours: number }>;
// Referral (IRF/ORF files)
referralFrom?: string;
referralTo?: string;
referralReason?: string;
referralDate?: string;
// Auto-tagger results (from Phase 2)
autoTagResult?: AutoTagResult;
}
Data Assembly Edge Function
Before generating files, the connector needs to assemble Encounter16FileContext from multiple sources. New function assemble16FileContext():
1. Load encounter from encounter_journey_cache (Supabase read model)
2. Load patient from MongoDB via NestJS proxy (PAT fields)
3. Load charges from fpa_fact_charge (charge details)
4. Load auto-tag results from nhso-auto-tagger (project codes, ADP mappings)
5. Load terminology mappings from terminology_cache (TMT/TMLT codes)
6. Load scheme info from gold_dim_scheme (INSCL code)
7. Merge into Encounter16FileContext
Export Artifact Storage
Generated ZIP files are uploaded to Supabase Storage bucket eclaim-exports/ with path: {hospitalCode}/{schemeCode}/{batchNumber}/{timestamp}.zip.
Dependencies
- Phase 0 (nhso_chrgitem_map for CHA/CHT file generation)
- Phase 1 (TMT/TMLT for drug/lab lines in ADP file)
- Phase 2 (auto-tagger for ADP code population)
- Existing:
eclaim-connector,fpa_fact_charge,encounter_journey_cache
Deliverables
- [ ] Generator module
generators/nhso-16files.ts(~1500 LOC) - [ ] Schema definition
generators/nhso-16files-schema.json - [ ] Context assembler
generators/assemble-16file-context.ts - [ ] Supabase Storage bucket for exports
- [ ] UI: “Export 16-File” button on
ClaimBatchPanel.tsx - [ ] UI: File preview/download dialog
- [ ] E2E test with sample data
Phase 4 — HIPData / Authen Code Integration
Goal: Verify patient insurance eligibility and retrieve authentication codes from NHSO’s HIPData system in real-time.
Edge Function: medbase/functions/hipdata-connector/
┌─────────────────────────────────────────────────────────────┐
│ hipdata-connector (Deno Edge Function) │
│ │
│ Actions: │
│ verifyEligibility — CID → scheme + hospmain + hospsub │
│ checkAuthenCode — CID + visit → authen code status │
│ fetchAuthenCode — pull authen code from NHSO system │
│ batchVerify — bulk eligibility check for batch │
│ │
│ Config: connector-packs/th-nhso-hipdata.json │
│ - Endpoint URLs, auth (X.509 certificate or API key) │
│ - Response mapping │
│ - Timeout and retry settings │
└─────────────────────────────────────────────────────────────┘
Persistence
New table (extend migration):
CREATE TABLE IF NOT EXISTS nhso_eligibility_cache (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
citizen_id TEXT NOT NULL,
scheme_code TEXT,
inscl_code TEXT,
hospmain TEXT,
hospsub TEXT,
effective_date DATE,
expire_date DATE,
authen_code TEXT,
authen_date TIMESTAMPTZ,
authen_method TEXT, -- 'id_card', 'mobile_app', 'kiosk', 'fingerprint'
verified_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
raw_response JSONB DEFAULT '{}',
UNIQUE(citizen_id, scheme_code)
);
Frontend Integration
- Patient registration — when CID is entered, auto-verify eligibility. Show scheme + hospmain in the scheme picker.
- Encounter creation — verify eligibility is current. Warn if expired.
- Billing screen — “Check Authen Code” button that calls
fetchAuthenCode. Display status badge: verified (green) / missing (red) / expired (yellow). - Batch submission — block encounters without valid authen codes.
Dependencies
- External: NHSO HIPData API access (X.509 certificate or API key from hospital registration)
- Phase 0 (scheme_code consistency)
Deliverables
- [ ] Edge function
medbase/functions/hipdata-connector/(index.ts, types.ts) - [ ] Connector config
connector-packs/th-nhso-hipdata.json - [ ] Persistence table
nhso_eligibility_cache - [ ] NestJS proxy
services/financial/.../hipdataConnector/ - [ ] Patient registration eligibility auto-check
- [ ] Billing screen authen code verification
- [ ] BRE evaluator:
authenCodePresence(BLOCK if missing)
Phase 5 — Revenue Optimization Engine
Goal: Proactively suggest coding improvements that increase DRG weight / adjRW without upcoding.
Edge Function: medbase/functions/revenue-optimizer/
Input: EncounterCodingContext (diagnoses, procedures, charges, DRG, demographics)
Output: RevenueSuggestion[] (each with estimated RW impact)
Evaluators:
1. CC/MCC Gap Detector
- Check if any documented conditions qualify as CC/MCC but are NOT coded
- Cross-reference clinical notes (if available) with ICD-10 CC/MCC lists
- "Documentation says 'acute kidney injury' but no N17.x code → +0.4 adjRW"
2. Specificity Upgrader
- Detect .9 (unspecified) codes that could be more specific
- "E11.9 (Type 2 DM unspecified) → E11.65 (with hyperglycemia) if BG > 250"
- Estimate adjRW delta between current and suggested DRG
3. Procedure Completeness
- Check if performed procedures are missing from the coded list
- Cross-reference OR log, medication admin, lab orders
- "Chest tube insertion documented but no 34.04 coded"
4. SDx Capture (Secondary Diagnoses)
- Identify conditions present on admission that affect DRG grouping
- Chronic conditions (HTN, DM, CKD) that are undertreated in coding
- "Patient on metformin → E11.x should be coded as SDx"
5. ADP Opportunity Detector
- Check for services rendered that qualify for ADP but aren't tagged
- "Lab COVID test done but no ADP Type 15 / Code 36598 tagged"
- "Palliative visit but missing ADP code 30001"
6. Claim Track Optimizer
- Verify the encounter is on the optimal claim track
- "Currently tagged as standard but patient is from Province X → OP Anywhere"
Country Pack: revenue-optimizer/country-packs/th-nhso-uc.json
{
"countryCode": "TH",
"schemeCode": "NHSO_UC",
"optimizers": {
"ccMccGap": {
"enabled": true,
"ccListSource": "terminology_cache", // query WHERE system='ICD10' AND properties->>'isCc' = 'true'
"mccListSource": "terminology_cache",
"minConfidence": 0.7,
"requireDocumentation": true
},
"specificityUpgrade": {
"enabled": true,
"unspecifiedPatterns": ["\\.9$", "\\.0$"],
"drgGrouper": "THAI_DRG_V5",
"baseRate": 8350,
"minRwDelta": 0.05 // only suggest if delta > 0.05 adjRW
},
"adpOpportunity": {
"enabled": true,
"adpMasterTable": "nhso_adp_master", // query for eligible codes
"feeScheduleTable": "nhso_fee_schedule"
},
"claimTrackOptimizer": {
"enabled": true
}
},
"messages": {
"REV_CC_GAP": {
"local": "ผู้ป่วยมีภาวะ {condition} แต่ยังไม่ลงรหัส {suggestedCode} — อาจเพิ่ม RW ได้ ~{rwDelta}",
"en": "Patient has {condition} but {suggestedCode} not coded — potential +{rwDelta} adjRW"
},
"REV_SPECIFICITY": {
"local": "รหัส {currentCode} เป็นรหัสไม่เจาะจง — พิจารณาใช้ {suggestedCode} เพื่อเพิ่ม RW {rwDelta}",
"en": "Code {currentCode} is unspecified — consider {suggestedCode} for +{rwDelta} adjRW"
},
"REV_ADP_MISSED": {
"local": "บริการ {serviceName} ยังไม่ติด ADP Type {adpType} Code {adpCode} — มูลค่า {amount} บาท",
"en": "Service {serviceName} missing ADP Type {adpType} Code {adpCode} — worth {amount} THB"
}
}
}
Suggestion Persistence
Uses existing rcm_ai_suggestion_log table with action_type: 'coding_suggestion':
{
"action_type": "coding_suggestion",
"encounter_id": "ENC-001",
"request_context": {
"optimizer": "ccMccGap",
"currentCodes": ["K80.20"],
"suggestedCode": "K80.21",
"estimatedRwDelta": 0.4,
"estimatedRevenueDelta": 3340 // 0.4 * 8350
},
"response_text": "ผู้ป่วยมีภาวะ obstruction แต่ยังไม่ลงรหัส K80.21",
"confidence": 0.85,
"user_accepted": null // pending coder review
}
Frontend: Revenue Suggestion Panel
New panel in rcm-kit: RevenueSuggestionPanel.tsx
- Shows per-encounter suggestions grouped by type (CC/MCC, Specificity, ADP, etc.)
- Each suggestion shows: current code → suggested code, estimated revenue impact, confidence
- Accept/Reject buttons that update
user_acceptedinrcm_ai_suggestion_log - Accept auto-updates the encounter’s coding (via BRE re-evaluation)
Dependencies
- Phase 0 (nhso_adp_master for ADP opportunity detection)
- Phase 1 (terminology_cache for CC/MCC lists, code lookups)
- Phase 2 (auto-tagger for claim track optimization)
- Existing:
rcm_ai_suggestion_log,coding_worklist,fpa_dim_diagnosis
Deliverables
- [ ] Edge function
medbase/functions/revenue-optimizer/(index.ts, types.ts, evaluators.ts) - [ ] Country pack
revenue-optimizer/country-packs/th-nhso-uc.json - [ ] NestJS proxy
services/financial/.../revenueOptimizer/ - [ ] Frontend
RevenueSuggestionPanel.tsx - [ ] Coding worklist integration (suggestions appear inline)
- [ ] Acceptance workflow (accept → re-code → re-evaluate BRE)
- [ ] Monthly aggregate: “Revenue Captured vs Revenue Suggested” report
Phase 6 — Charge-Level Reconciliation
Goal: Match each charge line to its government-approved/denied status so finance can investigate underpayments at the item level.
Migration: 101_charge_reconciliation.sql
CREATE TABLE IF NOT EXISTS rcm_charge_reconciliation (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
encounter_id TEXT NOT NULL,
statement_line_id UUID REFERENCES eclaim_statement_line(id),
charge_line_id BIGINT REFERENCES fpa_fact_charge(id),
-- Submitted values
submitted_charge_code TEXT,
submitted_nhso_category TEXT,
submitted_adp_type SMALLINT,
submitted_adp_code TEXT,
submitted_amount NUMERIC(14,2),
-- Government response (per line)
approved_amount NUMERIC(14,2),
denied_amount NUMERIC(14,2),
adjustment_reason TEXT,
adjustment_code TEXT, -- NHSO denial reason code
-- Status
recon_status TEXT DEFAULT 'pending' CHECK (recon_status IN (
'pending', -- not yet reconciled
'matched', -- amounts match
'underpaid', -- approved < submitted
'denied', -- fully denied
'overpaid', -- approved > submitted (rare)
'unmatched' -- can't find matching government line
)),
variance_amount NUMERIC(14,2) GENERATED ALWAYS AS (
COALESCE(submitted_amount, 0) - COALESCE(approved_amount, 0)
) STORED,
-- Investigation
investigated_by TEXT,
investigation_note TEXT,
investigated_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
Reconciliation Engine
New action in eclaim-connector: action: 'reconcileCharges'
1. Load fpa_fact_charge rows for the encounter
2. Load eclaim_statement_line for the encounter
3. Match charge lines to statement lines by:
a. Exact match: charge_code + amount
b. Category match: nhso_category + amount (±5%)
c. Fuzzy match: charge_date + category (for aggregated lines)
4. Compute variance per line
5. Insert into rcm_charge_reconciliation
6. Flag 'underpaid' and 'denied' lines for investigation
Frontend: Reconciliation Detail Dialog
Extend ReconciliationLedgerDetailDialog.tsx:
- Per-line table: charge item | submitted amount | approved amount | variance | status
- Color coding: green (matched), yellow (underpaid), red (denied), gray (unmatched)
- “Investigate” button per line → opens note dialog, assigns to user
- Aggregate summary: total submitted vs total approved vs total variance
Dependencies
- Phase 3 (charges must be submitted with per-line detail for government to respond per-line)
- Existing:
fpa_fact_charge,eclaim_statement_line
Deliverables
- [ ] Migration
101_charge_reconciliation.sql - [ ] Reconciliation engine in
eclaim-connector - [ ] Enhanced
ReconciliationLedgerDetailDialog.tsx - [ ] Variance investigation workflow
- [ ] Automated reconciliation trigger on statement import
Phase 7 — Appeal & Write-Off Workflow UI
Goal: Build the missing frontend for the existing rcm_denial_queue tables so finance staff can action D-cases and P-cases.
Components (in rcm-kit)
7a. Denial Triage Panel
- Worklist of open D/P cases from
rcm_denial_queue - Sortable by: variance amount, priority, deadline, denial type
- Quick actions: Assign, Start Review, Escalate
- Filters: full_denial vs partial_denial, scheme, date range
7b. Appeal Preparation Dialog
- Pre-fills encounter context (diagnoses, procedures, charges, original REP message)
- AI-assisted appeal letter generation (uses
coding-ai-assistantedge function) - Supporting document attachment
- Appeal strategy selector: full_appeal / partial_appeal / dispute_claim
- Review/approval workflow: preparer → reviewer → submit
7c. Appeal Batch Submission
- Groups appeal-prepared cases into a batch
- Submits via
eclaim-connectorwithsubmission_type: 'appeal' - Creates new
rcm_claim_batchrow linked to appeal - Updates
rcm_denial_queue.appeal_batch_id
7d. Write-Off Approval Workflow
- Write-off request form: reason (uncollectable/disputed/expired/policy/other), amount, justification
- Approval chain: requester → department head → finance director
- On approval: mark
workflow_status = 'written_off' - GL posting stub: insert into
fpa_fact_chargewithcharge_category: 'write_off'(actual GL integration is Phase 10+)
7e. Denial Analytics Dashboard
- Denial rate by scheme, department, diagnosis, month
- Top 10 denial reasons
- Average time to resolution
- Appeal success rate
- Write-off totals by category
Dependencies
- Existing:
rcm_denial_queuetable (020_rcm_denial_queue.sql) - Existing:
coding-ai-assistantedge function (for appeal letter generation) - Phase 6 (charge-level detail for appeal evidence)
Deliverables
- [ ]
DenialTriagePanel.tsx - [ ]
AppealPreparationDialog.tsx - [ ]
AppealBatchPanel.tsx - [ ]
WriteOffApprovalDialog.tsx - [ ]
DenialAnalyticsDashboard.tsx - [ ] Denial queue status indicators on
DashboardOverviewPanel.tsx
Phase 8 — Fee Schedule & Project Code Engine
Goal: Auto-prompt eligible prevention/promotion services at point of care, and auto-inject project codes per department configuration.
Fee Schedule Trigger
At departments flagged with show_nhso_fee_schedule = true:
- When an encounter is opened for billing, load
nhso_fee_scheduleitems matching the department - Check
auto_trigger_conditionsagainst patient demographics (age, gender, last service date) - Display eligible items as a checklist: “These services are eligible for this patient today”
- Pre-check items not yet billed; gray out items already billed
Department-Level Project Code Configuration
New admin screen at /admin/department-nhso-config:
- Per department: toggle
show_nhso_fee_schedule, select default project codes, set time-window rules - Stored in
department_nhso_configtable (or extend existing department master) - This replaces HOSxP’s
kskdepartment.show_nhso_fee_scheduleflag
Frontend: Fee Schedule Prompt
New component FeeSchedulePrompt.tsx:
- Renders as a banner/card at top of cashier billing screen
- Shows eligible items with unit prices
- “Add to Bill” button per item
- “Add All” button for convenience
- Dismissable per visit (stored in encounter metadata)
Dependencies
- Phase 0 (nhso_fee_schedule table)
- Phase 2 (auto-tagger provides fee schedule eligibility data)
Deliverables
- [ ]
FeeSchedulePrompt.tsxcomponent - [ ] Department NHSO config admin page
- [ ] Auto-trigger evaluation logic in auto-tagger
- [ ] Bilingual fee schedule display (Thai + English)
Phase 9 — Revenue-at-Risk Dashboard
Goal: Single-screen executive view showing how much money is being left on the table and where to focus.
Dashboard Sections
9a. Revenue Leakage Waterfall
Gross Charges → Contractual Adj → Discounts → Net Revenue → Collections → Write-offs
↑
show gap here (revenue at risk)
9b. Coding Opportunity Score
- Per-month aggregate: sum of all
estimatedRevenueDeltafrom revenue optimizer suggestions - Split by: accepted vs pending vs rejected
- Trend line: “Are we capturing more over time?”
9c. Submission Timeliness
- Distribution of days-to-submission across encounters
- Late penalty exposure: sum of estimated late penalties per tier
- Projected savings if all submitted by day 30
9d. Denial Prevention Score
- Predicted denial rate (from BRE blockers resolved vs ignored)
- Actual denial rate (from REP responses)
- Delta: “Denial rate dropped from 12% to 7% since BRE activation”
9e. Auto-Tag Coverage
- % of encounters auto-tagged vs manually tagged vs untagged
- Revenue from auto-tagged project codes
- Fee schedule capture rate: eligible items billed / eligible items available
9f. Per-Scheme Profitability
- Revenue, cost, margin by scheme (NHSO_UC, SSS, CSMBS, etc.)
- DRG case mix index by scheme
- Average adjRW by department
Data Sources
All existing — no new tables needed:
fpa_fact_encounter(revenue, cost, margin, DRG, claim status)fpa_fact_charge(charge-level detail)rcm_financial_alert(BRE alert counts)rcm_rep_response(denial rates)rcm_denial_queue(appeal/write-off status)rcm_ai_suggestion_log(optimization suggestions)eclaim_government_sync(submission status)eclaim_statement(payment status)gold_dim_scheme(scheme metadata)
Frontend: RevenueAtRiskDashboard.tsx
New top-level panel in the revenue settlement workspace. Uses:
- Recharts for waterfall, trend lines, distributions
- MUI DataGrid Pro for detail drill-down
- Real-time subscription for live KPI updates
Dependencies
- Phase 5 (revenue optimizer data for coding opportunity score)
- Phase 6 (reconciliation data for variance analysis)
- Existing: all FP&A tables
Deliverables
- [ ]
RevenueAtRiskDashboard.tsx(main component) - [ ] 6 dashboard section components
- [ ] SQL materialized views for KPI aggregation (extend
092_fpa_aggregates.sql) - [ ] Cron-refreshed materialized views (register in
cron_jobs) - [ ] Executive PDF export (via
report-template-engine)
Dependency Graph
Phase 0: Charge Masters ──────────────────────────────────────────────
│ │
▼ │
Phase 1: TMT/TMLT Seeding ──────────┐ │
│ │ │
▼ ▼ │
Phase 2: Auto-Tagging Engine ───── Phase 8: Fee Schedule Engine │
│ │ │
▼ │ │
Phase 3: 16-File Generator ─────────┘ │
│ │
├──► Phase 4: HIPData Integration (parallel, independent) │
│ │
▼ │
Phase 5: Revenue Optimizer ───────────────────────────────────────────┤
│ │
▼ │
Phase 6: Charge Reconciliation ───────────────────────────────────────┤
│ │
▼ │
Phase 7: Appeal/Write-Off UI ─────────────────────────────────────────┘
│
▼
Phase 9: Revenue Dashboard (aggregates everything)
Parallelizable:
- Phase 4 can run in parallel with Phases 2-3
- Phase 7 can start after Phase 0 (tables exist already), but is enriched by Phase 6
- Phase 8 can start with Phase 2 (shared auto-tagger)
Estimated Scope
| Phase | New LOC (est.) | New Files | New Tables | New Edge Functions |
|---|---|---|---|---|
| 0 | ~500 SQL + ~800 UI | 8 | 5 | 0 |
| 1 | ~400 seed + ~300 UI | 6 | 0 (uses existing) | 0 |
| 2 | ~1200 engine + ~600 UI | 8 | 0 | 1 (nhso-auto-tagger) |
| 3 | ~1500 generator + ~200 UI | 5 | 0 | 0 (extends eclaim-connector) |
| 4 | ~800 connector + ~400 UI | 6 | 1 | 1 (hipdata-connector) |
| 5 | ~1000 engine + ~500 UI | 7 | 0 (uses existing) | 1 (revenue-optimizer) |
| 6 | ~300 SQL + ~600 UI | 4 | 1 | 0 (extends eclaim-connector) |
| 7 | ~2000 UI | 6 | 0 (uses existing) | 0 |
| 8 | ~600 UI + ~200 config | 4 | 1 (dept config) | 0 (extends auto-tagger) |
| 9 | ~1500 UI + ~300 SQL | 8 | 0 (uses MVs) | 0 |
| Total | ~12,700 | 62 | 8 | 3 |
Priority Recommendation
Must-have for production (Phases 0→2→3): Without charge masters + auto-tagging + 16-file generation, medOS can’t generate compliant NHSO claims from first principles. Everything else is optimization.
High-value quick wins (Phases 5, 7): Revenue optimizer and appeal UI have the most direct revenue impact — they surface money being missed and let staff act on denials.
Nice-to-have (Phases 4, 8, 9): HIPData integration, fee schedule prompting, and the executive dashboard are polish that improve workflow efficiency but aren’t blocking claim generation.
Market Pack Authoring Checklist
When deploying to a new Thai hospital, the minimum data capture from their existing system (HOSxP or other):
- ☐ Scheme master (with HIPData verification code)
- ☐ Charge category master →
nhso_chrgitem_maprows - ☐ Drug master (with TMT codes) →
terminology_cache+terminology_concept_map - ☐ Lab master (with TMLT codes) →
terminology_cache+terminology_concept_map - ☐ Service master (with ADP Type + Code) →
nhso_adp_master - ☐ Department flags (show_nhso_fee_schedule) → department NHSO config
- ☐ Project code services →
nhso_project_code_rule - ☐ ICD-9/10 with HOMEWARD/IMC overload rows →
terminology_cache - ☐
nondrugitems.ucef_codemarkers →nhso_ucef_marker - ☐ Fee schedule items →
nhso_fee_schedule