Skip to main content

Claims Service — Data Model

Status: populated Owner: TBD Last updated: 2026-04-18 Companion: SERVICE_OVERVIEW · Service Template · 02 DDD

ID Prefix Registry

PrefixAggregate / EntityExample
clm_ClaimRecordclm_01HXY8K9QT3VBZ...
cov_Coveragecov_01HXY8K9QT3VBZ...
elig_EligibilityTransactionelig_01HXY8K9QT3VBZ...
auth_PriorAuthorizationauth_01HXY8K9QT3VBZ...
rem_RemittanceRecordrem_01HXY8K9QT3VBZ...
dnl_DenialCasednl_01HXY8K9QT3VBZ...

All IDs are ULIDs (26-character, lexicographically sortable).


TypeScript Interfaces

export type ClaimId = string & { readonly _brand: 'ClaimId' };
export type CoverageId = string & { readonly _brand: 'CoverageId' };
export type EligibilityId = string & { readonly _brand: 'EligibilityId' };
export type AuthorizationId = string & { readonly _brand: 'AuthorizationId' };
export type RemittanceId = string & { readonly _brand: 'RemittanceId' };
export type DenialId = string & { readonly _brand: 'DenialId' };

export type ClaimStatus =
| 'draft' | 'ready' | 'submitted' | 'accepted'
| 'rejected' | 'partial_paid' | 'paid' | 'denied'
| 'appealed' | 'closed';

export type CoverageStatus = 'active' | 'inactive' | 'cancelled';
export type CoveragePriority = 'primary' | 'secondary' | 'tertiary';
export type SubmissionChannel = 'x12_837' | 'payer_rest' | 'xml' | 'file';

export interface Money {
amount: number;
currency: string; // ISO 4217
}

export interface ClaimLineItem {
lineNumber: number;
chargeId: string;
procedureCode: { system: 'CPT' | 'HCPCS'; code: string; modifier?: string };
serviceDate: { from: Date; to: Date };
billedAmount: Money;
units: number;
diagnosisPointers: number[]; // indices into claim.diagnosisCodes
}

export interface ClaimRecord {
id: ClaimId;
tenantId: string;
patientId: string;
encounterId: string;
coverageId: CoverageId;
billProviderId: string;
renderingProviderId?: string;
serviceDate: { from: Date; to: Date };
diagnosisCodes: string[];
lineItems: ClaimLineItem[];
totalBilled: Money;
status: ClaimStatus;
submissionChannel?: SubmissionChannel;
clearinghouseRef?: string;
payerClaimNumber?: string;
version: number;
fhirClaimId?: string;
createdAt: Date;
updatedAt: Date;
}

export interface Coverage {
id: CoverageId;
tenantId: string;
patientId: string;
payerId: string;
subscriberId: string;
groupNumber?: string;
planName: string;
priority: CoveragePriority;
relationship: string;
effectiveFrom: Date;
effectiveTo?: Date;
copay?: Money;
deductible?: Money;
outOfPocketMax?: Money;
status: CoverageStatus;
fhirCoverageId?: string;
createdAt: Date;
updatedAt: Date;
}

Postgres DDL

-- Claims table
CREATE TABLE claims (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
patient_id TEXT NOT NULL,
encounter_id TEXT NOT NULL,
coverage_id TEXT NOT NULL REFERENCES coverages(id),
bill_provider_id TEXT NOT NULL,
rendering_provider_id TEXT,
service_date_from DATE NOT NULL,
service_date_to DATE NOT NULL,
diagnosis_codes TEXT[] NOT NULL DEFAULT '{}',
line_items JSONB NOT NULL DEFAULT '[]',
total_billed_amount NUMERIC(12,2) NOT NULL,
total_billed_currency TEXT NOT NULL DEFAULT 'USD',
status TEXT NOT NULL DEFAULT 'draft',
submission_channel TEXT,
clearinghouse_ref TEXT,
payer_claim_number TEXT,
version INTEGER NOT NULL DEFAULT 1,
fhir_claim_id TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

ALTER TABLE claims ENABLE ROW LEVEL SECURITY;
CREATE POLICY claims_tenant_isolation ON claims
USING (tenant_id = current_setting('app.tenant_id'));

CREATE INDEX idx_claims_tenant_patient ON claims(tenant_id, patient_id);
CREATE INDEX idx_claims_tenant_encounter ON claims(tenant_id, encounter_id);
CREATE INDEX idx_claims_tenant_status ON claims(tenant_id, status);
CREATE INDEX idx_claims_created_at ON claims(created_at DESC);

-- Coverages table
CREATE TABLE coverages (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
patient_id TEXT NOT NULL,
payer_id TEXT NOT NULL,
subscriber_id TEXT NOT NULL,
group_number TEXT,
plan_name TEXT NOT NULL,
priority TEXT NOT NULL DEFAULT 'primary',
relationship TEXT NOT NULL DEFAULT 'self',
effective_from DATE NOT NULL,
effective_to DATE,
copay_amount NUMERIC(10,2),
copay_currency TEXT,
deductible_amount NUMERIC(10,2),
deductible_currency TEXT,
oop_max_amount NUMERIC(10,2),
oop_max_currency TEXT,
status TEXT NOT NULL DEFAULT 'active',
fhir_coverage_id TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

ALTER TABLE coverages ENABLE ROW LEVEL SECURITY;
CREATE POLICY coverages_tenant_isolation ON coverages
USING (tenant_id = current_setting('app.tenant_id'));

CREATE INDEX idx_coverages_tenant_patient ON coverages(tenant_id, patient_id);
CREATE INDEX idx_coverages_tenant_status ON coverages(tenant_id, status);

-- Eligibility transactions table
CREATE TABLE eligibility_transactions (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
coverage_id TEXT NOT NULL REFERENCES coverages(id),
patient_id TEXT NOT NULL,
requested_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
channel TEXT NOT NULL,
request_payload JSONB NOT NULL,
response_payload JSONB,
status TEXT NOT NULL DEFAULT 'pending',
deductible_met_amount NUMERIC(10,2),
oop_met_amount NUMERIC(10,2),
coinsurance NUMERIC(5,2),
expires_at TIMESTAMPTZ,
fhir_eligibility_request_id TEXT,
fhir_eligibility_response_id TEXT
);

ALTER TABLE eligibility_transactions ENABLE ROW LEVEL SECURITY;
CREATE POLICY eligibility_tenant_isolation ON eligibility_transactions
USING (tenant_id = current_setting('app.tenant_id'));

CREATE INDEX idx_eligibility_coverage ON eligibility_transactions(tenant_id, coverage_id, requested_at DESC);

-- Prior authorizations table
CREATE TABLE prior_authorizations (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
coverage_id TEXT NOT NULL REFERENCES coverages(id),
patient_id TEXT NOT NULL,
procedure_code JSONB NOT NULL,
diagnosis_codes TEXT[] NOT NULL DEFAULT '{}',
requested_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
status TEXT NOT NULL DEFAULT 'pending',
authorization_number TEXT,
approved_units INTEGER,
valid_from DATE,
valid_to DATE,
denial_reason TEXT
);

ALTER TABLE prior_authorizations ENABLE ROW LEVEL SECURITY;
CREATE POLICY auth_tenant_isolation ON prior_authorizations
USING (tenant_id = current_setting('app.tenant_id'));

-- Remittances table
CREATE TABLE remittances (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
payer_id TEXT NOT NULL,
check_number TEXT,
payment_date DATE NOT NULL,
total_paid_amount NUMERIC(12,2) NOT NULL,
total_paid_currency TEXT NOT NULL DEFAULT 'USD',
raw_payload JSONB NOT NULL,
line_allocations JSONB NOT NULL DEFAULT '[]',
processed_at TIMESTAMPTZ,
fhir_eob_ids TEXT[] DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

ALTER TABLE remittances ENABLE ROW LEVEL SECURITY;
CREATE POLICY remittances_tenant_isolation ON remittances
USING (tenant_id = current_setting('app.tenant_id'));

-- Denial cases table
CREATE TABLE denial_cases (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
claim_id TEXT NOT NULL REFERENCES claims(id),
denial_code TEXT NOT NULL,
denial_reason TEXT NOT NULL,
appeal_deadline DATE NOT NULL,
appeal_status TEXT NOT NULL DEFAULT 'none',
appeal_filed_at TIMESTAMPTZ,
appeal_notes TEXT,
resubmit_claim_id TEXT REFERENCES claims(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

ALTER TABLE denial_cases ENABLE ROW LEVEL SECURITY;
CREATE POLICY denials_tenant_isolation ON denial_cases
USING (tenant_id = current_setting('app.tenant_id'));

-- Outbox table
CREATE TABLE claims_outbox (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
event_type TEXT NOT NULL,
payload JSONB NOT NULL,
published BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_claims_outbox_unpublished ON claims_outbox(published, created_at)
WHERE published = FALSE;

Migrations

MigrationDescription
001_create_coveragesInitial coverages table with RLS
002_create_claimsClaims table with RLS and indexes
003_create_eligibility_transactionsEligibility transaction table
004_create_prior_authorizationsPrior authorization table
005_create_remittances_and_denialsRemittances, denial_cases tables
006_create_claims_outboxTransactional outbox table
007_add_fhir_id_columnsfhir_claim_id, fhir_coverage_id, fhir_eob_ids columns