9bca4b608a
Backend:
- New POST /api/batches/{id}/export-837: regenerate X12 837 files
for a list of claim_ids into a ZIP using HCPF file naming standards,
with a unique interchange/group control number per export. Wire
the clearhouse Loop 1000A (NM1*41 + PER) and per-payer receiver
(NM1*40) blocks so the serializer no longer falls back to
CYCLONE / RECEIVER placeholders.
- /api/parse-837 and /api/parse-835 now surface the server-side
batch_id in both JSON and NDJSON response shapes so the frontend
can hit batch-scoped endpoints without an extra listBatches
round-trip.
- Filename helpers and the 837 serializer updated to match the new
HCPF envelope; tests cover batch export, parse batch_id, and the
serializer's control-number uniqueness guarantee.
Frontend:
- New shared components: ClaimCard, ClaimCard837, DominantKpiCard,
EditorialNote, ExportBar, TickerTape, and a charts/ set
(BarChart, HBarChart, SegmentedBar, AgingBars).
- New useBatchExport hook driving ExportBar's download flow against
the new endpoint.
- ClaimDrawer, Lane, and Layout migrated from raw CSS-variable
colors to Tailwind theme tokens (bg-card, text-foreground,
border/60, etc.) for consistency with the rest of the instrument
chrome; the active tab indicator gains a subtle accent glow.
- Upload, Inbox, Batches, BatchDiff, Reconciliation, and Acks pages
reworked to compose the new shared components and consume the new
batch-scoped API surface (notably ExportBar wired into Batches).
Tooling / Docs:
- Add audit-uiux.mjs and a docs/goodclaim.x12 sample fixture.
- Update ClaimDrawer testids and add coverage for the new
components and the useBatchExport hook.
Rolls up into the v0.2.0 release tag.
863 lines
24 KiB
TypeScript
863 lines
24 KiB
TypeScript
// ---------------------------------------------------------------------------
|
|
// UI-shaped types (used by the existing pages; backed by in-memory sample data
|
|
// when no API is configured). Keep these stable — the rest of the app reads
|
|
// from them.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export type ClaimStatus =
|
|
| "draft"
|
|
| "submitted"
|
|
| "accepted"
|
|
| "denied"
|
|
| "paid"
|
|
| "pending";
|
|
|
|
export type RemittanceStatus = "received" | "posted" | "reconciled";
|
|
|
|
export type ActivityKind =
|
|
| "claim_submitted"
|
|
| "claim_paid"
|
|
| "claim_denied"
|
|
| "claim_accepted"
|
|
| "remit_received"
|
|
| "provider_added"
|
|
// Emitted by `cyclone.store.record_manual_match` when an operator
|
|
// pairs a remit to a claim by hand. Surface as a neutral event on
|
|
// the Activity Log; do NOT treat it as a status change.
|
|
| "manual_match";
|
|
|
|
export interface Claim {
|
|
id: string;
|
|
patientName: string;
|
|
providerNpi: string;
|
|
payerName: string;
|
|
cptCode: string;
|
|
billedAmount: number;
|
|
receivedAmount: number;
|
|
status: ClaimStatus;
|
|
submissionDate: string;
|
|
denialReason?: string;
|
|
}
|
|
|
|
export interface Provider {
|
|
npi: string;
|
|
name: string;
|
|
taxId: string;
|
|
address: string;
|
|
city: string;
|
|
state: string;
|
|
zip: string;
|
|
phone: string;
|
|
claimCount: number;
|
|
outstandingAr: number;
|
|
/**
|
|
* SP21 Task 1.6: populated by the extended `GET /api/config/providers/{npi}`
|
|
* endpoint. Top 10 claims for this provider by `submissionDate` desc.
|
|
* Optional so legacy callers (in-memory sample data) keep working
|
|
* without an API round-trip.
|
|
*/
|
|
recent_claims?: ClaimSummary[];
|
|
/**
|
|
* SP21 Task 1.6: populated by the extended `GET /api/config/providers/{npi}`
|
|
* endpoint. Top 10 activity events (by `ts` desc) joined to claims
|
|
* owned by this provider via `claim_id`.
|
|
*/
|
|
recent_activity?: ActivityEvent[];
|
|
}
|
|
|
|
/**
|
|
* SP21 Task 1.6: slim claim projection returned by
|
|
* `GET /api/config/providers/{npi}.recent_claims`. Mirrors the wire
|
|
* shape of `store.iter_claims(provider_npi=...)` 1:1 (camelCase,
|
|
* superset of the legacy `Claim` interface).
|
|
*/
|
|
export interface ClaimSummary {
|
|
id: string;
|
|
state: string;
|
|
billedAmount: number;
|
|
patientName: string;
|
|
providerNpi: string;
|
|
payerName: string;
|
|
cptCode: string;
|
|
submissionDate: string;
|
|
parsedAt: string;
|
|
status: string;
|
|
matchedRemittanceId?: string | null;
|
|
batchId: string;
|
|
receivedAmount?: number;
|
|
denialReason?: string | null;
|
|
}
|
|
|
|
/**
|
|
* SP21 Task 1.6: one row in `recent_activity`. Mirrors the Python
|
|
* ORM `ActivityEvent` (snake_case fields rewritten to camelCase for
|
|
* the wire). `payload` carries the same dict the recorder wrote at
|
|
* the time of the event (message, npi, amount, etc.).
|
|
*/
|
|
export interface ActivityEvent {
|
|
id: number;
|
|
ts: string; // ISO Z
|
|
kind: string;
|
|
batchId: string | null;
|
|
claimId: string | null;
|
|
remittanceId: string | null;
|
|
payload: Record<string, unknown>;
|
|
}
|
|
|
|
export interface Remittance {
|
|
id: string;
|
|
claimId: string;
|
|
payerName: string;
|
|
paidAmount: number;
|
|
adjustmentAmount: number;
|
|
receivedDate: string;
|
|
checkNumber: string;
|
|
status: RemittanceStatus;
|
|
denialReason?: string | null;
|
|
validationWarnings?: string[];
|
|
/**
|
|
* Persisted CAS adjustment rows, labeled by the backend via the bundled
|
|
* CARC dictionary. Optional — the list endpoint omits it to keep the
|
|
* payload small; the detail endpoint (`/api/remittances/{id}`) populates it.
|
|
*/
|
|
adjustments?: CasAdjustment[];
|
|
/**
|
|
* SP7: per-line 835 SVC composites on this remittance. Ordered by
|
|
* ``lineNumber``. Each SLP carries the per-line CAS adjustments under
|
|
* ``serviceLinePayments[i].adjustments`` via the line-reconciliation
|
|
* endpoint, or via a follow-up CAS query in the remit drawer.
|
|
*/
|
|
serviceLinePayments?: ServiceLinePayment[];
|
|
/**
|
|
* SP7: CLP-level (claim-level) CAS bucket — adjustments that were
|
|
* not attached to any SVC composite. Powers the ClaimLevelAdjustments
|
|
* panel in the remit drawer.
|
|
*/
|
|
claimLevelAdjustments?: RemittanceClaimLevelAdjustment[];
|
|
}
|
|
|
|
/**
|
|
* One Claim Adjustment Segment (CAS) row, labeled.
|
|
* Source: backend `cyclone.parsers.cas_codes.reason_label(group, reason)`.
|
|
*/
|
|
export interface CasAdjustment {
|
|
group: string; // "CO", "PR", "OA", "PI", "CR"
|
|
reason: string; // "45", "1", etc.
|
|
label: string; // "Charge exceeds fee schedule"
|
|
amount: number;
|
|
quantity: number | null;
|
|
}
|
|
|
|
export interface Activity {
|
|
id: string;
|
|
kind: ActivityKind;
|
|
message: string;
|
|
timestamp: string;
|
|
npi?: string;
|
|
amount?: number;
|
|
/**
|
|
* SP21 Task 2.5: read from the backend `ActivityEvent.claim_id` so
|
|
* the Dashboard "Recent activity" card can route a click on a
|
|
* `claim_*` event to the matching claim drawer (via
|
|
* `src/lib/event-routing.ts`). Populated only for events that
|
|
* correspond to a specific claim; `null` for orphan events such as
|
|
* `remit_received` or `provider_added`.
|
|
*/
|
|
claimId?: string | null;
|
|
/**
|
|
* SP21 Task 2.5: read from the backend `ActivityEvent.remittance_id`.
|
|
* Populated only for events that reference a specific remittance
|
|
* (e.g. `remit_received`); `null` otherwise. The Dashboard toast
|
|
* covers the Phase 2 case where this routes to a not-yet-built
|
|
* `RemitDrawer` (Phase 4).
|
|
*/
|
|
remittanceId?: string | null;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Backend-aligned types (snake_case to match FastAPI JSON output 1:1).
|
|
// These mirror the Pydantic models in
|
|
// backend/src/cyclone/parsers/models.py (837P)
|
|
// backend/src/cyclone/parsers/models_835.py (835 ERA)
|
|
// Numeric fields are strings (Decimals serialized); dates are ISO strings.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// --- 837P ------------------------------------------------------------------
|
|
|
|
export interface Address {
|
|
line1: string;
|
|
line2?: string | null;
|
|
city: string;
|
|
state: string;
|
|
zip: string;
|
|
}
|
|
|
|
export interface BillingProvider {
|
|
name: string;
|
|
npi: string;
|
|
tax_id?: string | null;
|
|
address?: Address | null;
|
|
}
|
|
|
|
export interface Subscriber {
|
|
first_name: string;
|
|
last_name: string;
|
|
member_id: string;
|
|
dob?: string | null;
|
|
gender?: "M" | "F" | "U" | null;
|
|
address?: Address | null;
|
|
}
|
|
|
|
export interface Payer {
|
|
name: string;
|
|
id: string;
|
|
}
|
|
|
|
export interface ClaimHeader {
|
|
claim_id: string;
|
|
total_charge: string; // Decimal serialized
|
|
place_of_service?: string | null;
|
|
facility_code_qualifier?: string | null;
|
|
frequency_code?: string | null;
|
|
provider_signature?: string | null;
|
|
assignment?: string | null;
|
|
release_of_info?: string | null;
|
|
prior_auth?: string | null;
|
|
}
|
|
|
|
export interface Diagnosis {
|
|
code: string;
|
|
qualifier?: string | null;
|
|
}
|
|
|
|
export interface Procedure {
|
|
qualifier: string;
|
|
code: string;
|
|
modifiers: string[];
|
|
}
|
|
|
|
export interface ServiceLine {
|
|
line_number: number;
|
|
procedure: Procedure;
|
|
charge: string; // Decimal serialized
|
|
unit_type?: string | null;
|
|
units?: string | null; // Decimal serialized
|
|
place_of_service?: string | null;
|
|
service_date?: string | null;
|
|
provider_reference?: string | null;
|
|
}
|
|
|
|
export interface ValidationIssue {
|
|
rule: string;
|
|
severity: "error" | "warning";
|
|
message: string;
|
|
segment_index?: number | null;
|
|
}
|
|
|
|
export interface ValidationReport {
|
|
passed: boolean;
|
|
errors: ValidationIssue[];
|
|
warnings: ValidationIssue[];
|
|
}
|
|
|
|
export interface Envelope {
|
|
sender_id: string;
|
|
receiver_id: string;
|
|
control_number: string;
|
|
transaction_date: string; // ISO date
|
|
transaction_time?: string | null;
|
|
implementation_guide?: string | null;
|
|
}
|
|
|
|
export interface BatchSummary {
|
|
input_file: string;
|
|
control_number?: string | null;
|
|
transaction_date?: string | null;
|
|
total_claims: number;
|
|
passed: number;
|
|
failed: number;
|
|
failed_claim_ids: string[];
|
|
issues_by_rule: Record<string, number>;
|
|
output_dir?: string | null;
|
|
}
|
|
|
|
export interface ClaimOutput {
|
|
claim_id: string;
|
|
control_number: string;
|
|
transaction_date: string; // ISO date
|
|
billing_provider: BillingProvider;
|
|
subscriber: Subscriber;
|
|
payer: Payer;
|
|
claim: ClaimHeader;
|
|
diagnoses: Diagnosis[];
|
|
service_lines: ServiceLine[];
|
|
validation: ValidationReport;
|
|
raw_segments: string[][];
|
|
}
|
|
|
|
export interface ParseResult837 {
|
|
envelope: Envelope | null;
|
|
claims: ClaimOutput[];
|
|
summary: BatchSummary;
|
|
/**
|
|
* Server-side batch id (UUID). Present on the JSON response from
|
|
* POST /api/parse-837 — null/missing on the NDJSON-streaming path
|
|
* (use the final `summary.batch_id` from the NDJSON stream instead).
|
|
* Required by /api/batches/{batch_id}/export-837.
|
|
*/
|
|
batch_id?: string | null;
|
|
}
|
|
|
|
// --- 835 ERA ---------------------------------------------------------------
|
|
|
|
export interface FinancialInfo {
|
|
handling_code: string;
|
|
paid_amount: string; // Decimal serialized
|
|
credit_debit_flag: string;
|
|
payment_method?: string | null;
|
|
payer_tax_id?: string | null;
|
|
payment_date?: string | null; // ISO date
|
|
}
|
|
|
|
export interface ReassociationTrace {
|
|
trace_type_code: string;
|
|
trace_number: string;
|
|
originating_company_id: string;
|
|
payer_id?: string | null;
|
|
}
|
|
|
|
export interface Payer835 {
|
|
name: string;
|
|
id?: string | null;
|
|
address?: Address | null;
|
|
contact_url?: string | null;
|
|
}
|
|
|
|
export interface Payee835 {
|
|
name: string;
|
|
npi: string;
|
|
address?: Address | null;
|
|
}
|
|
|
|
export interface ClaimAdjustment {
|
|
group_code: string;
|
|
reason_code: string;
|
|
amount: string; // Decimal serialized
|
|
quantity?: string | null;
|
|
}
|
|
|
|
export interface ServicePayment {
|
|
line_number: number;
|
|
procedure_qualifier: string;
|
|
procedure_code: string;
|
|
modifiers: string[];
|
|
charge: string;
|
|
payment: string;
|
|
units?: string | null;
|
|
unit_type?: string | null;
|
|
service_date?: string | null;
|
|
adjustments: ClaimAdjustment[];
|
|
ref_benefit_plan?: string | null;
|
|
}
|
|
|
|
export interface ClaimPayment {
|
|
payer_claim_control_number: string;
|
|
status_code: string;
|
|
status_label?: string | null;
|
|
total_charge: string;
|
|
total_paid: string;
|
|
patient_responsibility?: string | null;
|
|
claim_filing_indicator?: string | null;
|
|
original_claim_id?: string | null;
|
|
facility_type?: string | null;
|
|
frequency_code?: string | null;
|
|
per_diem_covered_days?: string | null;
|
|
ref_benefit_plan?: string | null;
|
|
service_payments: ServicePayment[];
|
|
raw_segments: string[][];
|
|
}
|
|
|
|
export interface ParseResult835 {
|
|
envelope: Envelope;
|
|
financial_info: FinancialInfo;
|
|
trace: ReassociationTrace;
|
|
payer: Payer835;
|
|
payee: Payee835;
|
|
claims: ClaimPayment[];
|
|
summary: BatchSummary;
|
|
validation?: ValidationReport | null;
|
|
/** See ParseResult837.batch_id. */
|
|
batch_id?: string | null;
|
|
}
|
|
|
|
// --- NDJSON streaming ------------------------------------------------------
|
|
// The backend emits one JSON object per line during an NDJSON stream:
|
|
// { "type": "<event>", "data": <object> }
|
|
// See backend/src/cyclone/api.py:_ndjson_stream* for the source of truth.
|
|
|
|
export type NdjsonEventType =
|
|
| "envelope"
|
|
| "claim"
|
|
| "claim_payment"
|
|
| "financial_info"
|
|
| "trace"
|
|
| "payer"
|
|
| "payee"
|
|
| "summary";
|
|
|
|
export interface NdjsonEvent<T = unknown> {
|
|
type: NdjsonEventType;
|
|
data: T;
|
|
}
|
|
|
|
// --- Parsed-batch slice (used by the Upload page + store) ------------------
|
|
|
|
export type ParsedBatchKind = "837p" | "835";
|
|
|
|
export interface ParsedBatch {
|
|
id: string;
|
|
kind: ParsedBatchKind;
|
|
inputFilename: string;
|
|
parsedAt: string; // ISO timestamp
|
|
claimCount: number;
|
|
passed: number;
|
|
failed: number;
|
|
claimIds: string[];
|
|
summary: BatchSummary;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Reconciliation (7-state claim model + match audit trail)
|
|
// Mirrors cyclone.db.ClaimState (Python enum) 1:1. The reconciliation page
|
|
// uses these to drive the unmatched bucket and the manual-match UI.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export type ClaimState =
|
|
| "submitted"
|
|
| "received"
|
|
| "paid"
|
|
| "partial"
|
|
| "denied"
|
|
| "reconciled"
|
|
| "reversed";
|
|
|
|
export const CLAIM_STATES: ClaimState[] = [
|
|
"submitted",
|
|
"received",
|
|
"paid",
|
|
"partial",
|
|
"denied",
|
|
"reconciled",
|
|
"reversed",
|
|
];
|
|
|
|
export interface Match {
|
|
id: number;
|
|
claim_id: string;
|
|
remittance_id: string;
|
|
strategy: "auto" | "manual";
|
|
matched_at: string;
|
|
prior_claim_state?: ClaimState | null;
|
|
is_reversal: boolean;
|
|
}
|
|
|
|
// Mirrors `cyclone.store.to_ui_claim_from_orm` (backend/src/cyclone/store.py).
|
|
// Fields the backend does not currently emit are optional so the type
|
|
// matches the actual wire shape (was previously assumed but never true).
|
|
export interface UnmatchedClaim {
|
|
id: string;
|
|
patientName: string;
|
|
billedAmount: number;
|
|
providerNpi: string | null;
|
|
payerName?: string;
|
|
payerId?: string | null;
|
|
serviceDate?: string | null;
|
|
cptCode?: string;
|
|
submissionDate?: string;
|
|
state: ClaimState;
|
|
}
|
|
|
|
// Mirrors `cyclone.store.list_unmatched` (backend/src/cyclone/store.py) for
|
|
// the remittances payload. `status` is the 3-state remittance lifecycle
|
|
// ("received" | "posted" | "reconciled"); the previous `statusCode` field
|
|
// was a confusion with the 835 claim-status code and never existed on the
|
|
// wire. Fields the backend omits are optional.
|
|
export interface UnmatchedRemittance {
|
|
id: string;
|
|
payerClaimControlNumber: string;
|
|
payerName?: string;
|
|
status: string;
|
|
paidAmount: number;
|
|
adjustmentAmount: number;
|
|
batchId: string;
|
|
receivedDate?: string;
|
|
parsedAt?: string;
|
|
claimId?: string;
|
|
denialReason?: string | null;
|
|
validationWarnings?: string[];
|
|
isReversal?: boolean;
|
|
totalCharge?: number;
|
|
serviceDate?: string | null;
|
|
}
|
|
|
|
export interface UnmatchedResponse {
|
|
claims: UnmatchedClaim[];
|
|
remittances: UnmatchedRemittance[];
|
|
}
|
|
|
|
export interface MatchResponse {
|
|
claim: UnmatchedClaim;
|
|
match: { id: number; strategy: "auto" | "manual" };
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 999 ACKs (SP3 P3 T16)
|
|
// Mirrors the `acks` table (cyclone.db.Ack) and the `/api/acks` response.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* One persisted 999 ACK row, camelCased for the UI. The backend
|
|
* (snake_case) is re-shaped in `src/hooks/useAcks.ts` via the
|
|
* `mapAck` helper, so this interface matches what the page actually
|
|
* consumes.
|
|
*/
|
|
export interface Ack {
|
|
id: number;
|
|
sourceBatchId: string;
|
|
acceptedCount: number;
|
|
rejectedCount: number;
|
|
receivedCount: number;
|
|
ackCode: "A" | "E" | "R" | "P";
|
|
parsedAt: string;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// SP4 claim detail drawer types (Task 4).
|
|
// Mirrors the JSON shape of `GET /api/claims/{claim_id}` 1:1 — camelCase
|
|
// keys, numeric money fields (coerced from Decimal on the backend), ISO
|
|
// timestamps with a trailing `Z`. Distinct from the parser-aligned
|
|
// snake_case types above (e.g. ``BillingProvider``, ``ServiceLine``) which
|
|
// describe the EDI parse tree; these describe the UI-ready detail payload.
|
|
// The ``ClaimDetail*`` prefix keeps the two flavors side-by-side without
|
|
// a TypeScript duplicate-identifier collision.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export interface ClaimDetailDiagnosis {
|
|
code: string;
|
|
qualifier: string | null;
|
|
}
|
|
|
|
export interface ClaimDetailServiceLine {
|
|
lineNumber: number;
|
|
procedureQualifier: string;
|
|
procedureCode: string;
|
|
modifiers: string[];
|
|
charge: number;
|
|
units: number | null;
|
|
unitType: string | null;
|
|
serviceDate: string | null; // ISO date YYYY-MM-DD or null
|
|
}
|
|
|
|
/**
|
|
* The backend can return ``{}`` for a missing address (see
|
|
* ``store._address_to_ui``); callers that want a populated address must
|
|
* branch on ``Object.keys(addr).length === 0`` before reading fields.
|
|
*/
|
|
export interface ClaimDetailAddress {
|
|
line1: string;
|
|
line2: string | null;
|
|
city: string;
|
|
state: string;
|
|
zip: string;
|
|
}
|
|
|
|
export interface ClaimDetailBillingProvider {
|
|
name: string;
|
|
npi: string;
|
|
taxId: string;
|
|
address: ClaimDetailAddress | Record<string, never>;
|
|
}
|
|
|
|
export interface ClaimDetailSubscriber {
|
|
firstName: string;
|
|
lastName: string;
|
|
memberId: string;
|
|
dob: string | null;
|
|
gender: string | null;
|
|
}
|
|
|
|
export interface ClaimDetailPayer {
|
|
name: string;
|
|
id: string;
|
|
}
|
|
|
|
export interface ClaimDetailParties {
|
|
billingProvider: ClaimDetailBillingProvider;
|
|
subscriber: ClaimDetailSubscriber;
|
|
payer: ClaimDetailPayer;
|
|
}
|
|
|
|
export interface ClaimDetailValidationIssue {
|
|
rule: string;
|
|
severity: "error" | "warning";
|
|
message: string;
|
|
}
|
|
|
|
export interface ClaimDetailValidation {
|
|
passed: boolean;
|
|
errors: ClaimDetailValidationIssue[];
|
|
warnings: ClaimDetailValidationIssue[];
|
|
}
|
|
|
|
export interface ClaimDetailMatchedRemittance {
|
|
id: string;
|
|
totalPaid: number;
|
|
/** Mirrors the same `"received" | "reconciled"` mapping as the list endpoint. */
|
|
status: string;
|
|
receivedAt: string; // ISO Z
|
|
/**
|
|
* SP7: per-line counts for the badge. Optional for backward
|
|
* compatibility — older claim-detail responses (pre-SP7) won't have
|
|
* these fields. The card omits the badge when undefined.
|
|
*/
|
|
matchedLines?: number;
|
|
totalLines?: number;
|
|
}
|
|
|
|
export interface ClaimDetailStateHistoryEvent {
|
|
kind: string;
|
|
ts: string; // ISO Z (most-recent-first, capped at 50 by the backend)
|
|
batchId: string | null;
|
|
remittanceId: string | null;
|
|
}
|
|
|
|
/**
|
|
* One claim with the full drawer context: header, state, parties,
|
|
* validation, service lines, diagnoses, raw segments, recent
|
|
* ``stateHistory`` (most-recent-first, capped at 50), and a populated
|
|
* ``matchedRemittance`` block when the claim has been paired with an
|
|
* ERA. Mirrors ``CycloneStore.get_claim_detail`` (backend/src/cyclone/store.py)
|
|
* and the ``GET /api/claims/{claim_id}`` endpoint contract.
|
|
*/
|
|
export interface ClaimDetail {
|
|
id: string;
|
|
batchId: string;
|
|
state: string;
|
|
stateLabel: string;
|
|
billedAmount: number;
|
|
patientName: string;
|
|
providerNpi: string;
|
|
providerName: string;
|
|
payerName: string;
|
|
payerId: string;
|
|
submissionDate: string; // ISO Z
|
|
serviceDateFrom: string | null;
|
|
serviceDateTo: string | null;
|
|
parsedAt: string; // ISO Z
|
|
diagnoses: ClaimDetailDiagnosis[];
|
|
serviceLines: ClaimDetailServiceLine[];
|
|
parties: ClaimDetailParties;
|
|
validation: ClaimDetailValidation;
|
|
rawSegments: string[][];
|
|
matchedRemittance: ClaimDetailMatchedRemittance | null;
|
|
stateHistory: ClaimDetailStateHistoryEvent[];
|
|
/**
|
|
* SP7: per-line reconciliation slim projection. Parallel to
|
|
* ``serviceLines`` and indexed by ``lineNumber``. Populated by the
|
|
* claim-detail endpoint so the ServiceLinesTable can render Paid +
|
|
* Adjustments columns without a second fetch. ``paid`` /
|
|
* ``adjustmentsSum`` are null when the line has no matched 835 SVC
|
|
* composite.
|
|
*/
|
|
lineReconciliation?: ClaimDetailLineReconciliation[];
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Side-by-side batch diff (SP3 P4 / T18)
|
|
// Mirrors `GET /api/batch-diff?a=<batch_id>&b=<batch_id>` (see
|
|
// `backend/src/cyclone/batch_diff.py` and `backend/src/cyclone/api.py`).
|
|
// The contract is deliberately small: no raw segments, no service-line
|
|
// details — just the summary fields the operator needs to spot the
|
|
// deltas between two parsed files at a glance.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export type BatchKind = "837p" | "835";
|
|
|
|
export interface BatchDiffSideMeta {
|
|
id: string;
|
|
kind: BatchKind;
|
|
parsedAt: string; // ISO Z
|
|
inputFilename: string;
|
|
claimCount: number;
|
|
}
|
|
|
|
/**
|
|
* One claim (or remittance) row projected for the diff view. Carries
|
|
* just enough context to render side-by-side without the EDI parse
|
|
* tree. ``status`` carries the validation-derived label for 837P
|
|
* (``submitted`` / ``pending`` / ``draft`` / ``denied``) and the CLP02
|
|
* code/label for 835.
|
|
*/
|
|
export interface BatchClaimDiffSummary {
|
|
claim_id: string;
|
|
patient_name: string;
|
|
total_charge: number;
|
|
service_date: string | null;
|
|
status: string;
|
|
cpt_codes: string[];
|
|
}
|
|
|
|
/**
|
|
* One changed field between the two sides of a claim. ``field``
|
|
* identifies which column changed (``status`` / ``total_charge`` /
|
|
* ``service_date`` / ``cpt_codes``); ``a`` and ``b`` are the values
|
|
* from each side, rendered verbatim.
|
|
*/
|
|
export interface BatchDiffFieldDiff {
|
|
field: "status" | "total_charge" | "service_date" | "cpt_codes";
|
|
a: string | number | string[] | null;
|
|
b: string | number | string[] | null;
|
|
}
|
|
|
|
export interface BatchDiffChangedRow {
|
|
a: BatchClaimDiffSummary;
|
|
b: BatchClaimDiffSummary;
|
|
diff: BatchDiffFieldDiff[];
|
|
}
|
|
|
|
export interface BatchDiffSummary {
|
|
addedCount: number;
|
|
removedCount: number;
|
|
changedCount: number;
|
|
unchangedCount: number;
|
|
}
|
|
|
|
/**
|
|
* Full response from `GET /api/batch-diff`. The three bucket arrays
|
|
* (``added`` / ``removed`` / ``changed``) are always present (never
|
|
* absent) so the UI can index unconditionally; ``summary`` is
|
|
* precomputed by the backend so the four count cards don't have to
|
|
* derive them on the client.
|
|
*/
|
|
export interface BatchDiff {
|
|
a: BatchDiffSideMeta;
|
|
b: BatchDiffSideMeta;
|
|
added: BatchClaimDiffSummary[];
|
|
removed: BatchClaimDiffSummary[];
|
|
changed: BatchDiffChangedRow[];
|
|
summary: BatchDiffSummary;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// SP7 — Per-Service-Line Adjustment Audit types.
|
|
// See docs/superpowers/specs/2026-06-20-cyclone-line-reconciliation-design.md
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/** One 835 SVC composite (Loop 2110) projected to wire shape. */
|
|
export interface ServiceLinePayment {
|
|
id: number;
|
|
lineNumber: number;
|
|
procedureQualifier: string;
|
|
procedureCode: string;
|
|
modifiers: string[];
|
|
charge: string;
|
|
payment: string;
|
|
units: string | null;
|
|
unitType: string | null;
|
|
serviceDate: string | null;
|
|
}
|
|
|
|
/** Status of one line on either side of a reconciliation. */
|
|
export type LineReconciliationStatus =
|
|
| "matched"
|
|
| "unmatched_835_only"
|
|
| "unmatched_837_only"
|
|
| "superseded";
|
|
|
|
/** Slim per-line projection embedded in `GET /api/claims/{id}`. */
|
|
export interface ClaimDetailLineReconciliation {
|
|
lineNumber: number;
|
|
status: LineReconciliationStatus;
|
|
paid: string | null;
|
|
adjustmentsSum: string | null;
|
|
}
|
|
|
|
/** Summary block at the top of the line-reconciliation response. */
|
|
export interface LineReconciliationSummary {
|
|
billedTotal: string;
|
|
paidTotal: string;
|
|
adjustmentTotal: string;
|
|
matchedLines: number;
|
|
totalLines: number;
|
|
}
|
|
|
|
/** One line in the line-reconciliation response. */
|
|
export interface LineReconciliationRow {
|
|
claimServiceLine: {
|
|
lineNumber: number;
|
|
procedureQualifier: string;
|
|
procedureCode: string;
|
|
modifiers: string[];
|
|
charge: string;
|
|
units: string | null;
|
|
unitType: string | null;
|
|
serviceDate: string | null;
|
|
} | null;
|
|
serviceLinePayment: ServiceLinePayment | null;
|
|
status: LineReconciliationStatus;
|
|
adjustments: Array<{
|
|
groupCode: string;
|
|
reasonCode: string;
|
|
amount: string;
|
|
}>;
|
|
}
|
|
|
|
/** Top-level shape of `GET /api/claims/{id}/line-reconciliation`. */
|
|
export interface LineReconciliationResponse {
|
|
claimId: string;
|
|
summary: LineReconciliationSummary;
|
|
lines: LineReconciliationRow[];
|
|
}
|
|
|
|
/** One CLP-level (claim-level) CAS adjustment on a remittance. */
|
|
export interface RemittanceClaimLevelAdjustment {
|
|
id: number;
|
|
groupCode: string;
|
|
reasonCode: string;
|
|
amount: string;
|
|
quantity: string | null;
|
|
}
|
|
|
|
/** Counts embedded in inbox-lane rows for the `MatchedRemitCard` badge. */
|
|
export interface MatchedRemittanceLineCounts {
|
|
matchedLines: number;
|
|
totalLines: number;
|
|
}
|
|
|
|
/** Inbox-lane claim row carrying the matched-remittance block. */
|
|
export interface InboxClaimRow {
|
|
id: string;
|
|
kind: "claim" | "remit";
|
|
patientControlNumber: string;
|
|
chargeAmount: number | null;
|
|
payerId: string | null;
|
|
providerNpi: string | null;
|
|
state: string;
|
|
rejectionReason: string | null;
|
|
rejectedAt: string | null;
|
|
serviceDateFrom: string | null;
|
|
score: number | null;
|
|
scoreTier: string | null;
|
|
scoreBreakdown: {
|
|
patient: number;
|
|
date: number;
|
|
amount: number;
|
|
provider: number;
|
|
} | null;
|
|
matchedRemittance: {
|
|
id: string;
|
|
matchedLines: number;
|
|
totalLines: number;
|
|
} | null;
|
|
}
|