// --------------------------------------------------------------------------- // 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; } 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; } // --------------------------------------------------------------------------- // 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; 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; } // --- 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; } // --- NDJSON streaming ------------------------------------------------------ // The backend emits one JSON object per line during an NDJSON stream: // { "type": "", "data": } // 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 { 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; } 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=&b=` (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; }