990 lines
29 KiB
TypeScript
990 lines
29 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: "pcn-exact" | "score-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: "pcn-exact" | "score-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;
|
|
/**
|
|
* AK2 set_control_number from the inbound 999 — the
|
|
* patient_control_number of the original claim batch this 999
|
|
* acks. Surfaced in the list endpoint so the operator can
|
|
* correlate a 999 to a claim batch in the UI without a second
|
|
* round-trip to the detail endpoint.
|
|
*/
|
|
patientControlNumber?: string | null;
|
|
/**
|
|
* SP28: distinct claim ids this 999 ack is linked to. Populated by
|
|
* the backend list endpoint so the Acks page can show a "Claims"
|
|
* badge column without a per-row detail round-trip. Empty array
|
|
* means no auto-link resolved (orphan).
|
|
*/
|
|
linkedClaimIds?: string[];
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// TA1 (Interchange Acknowledgment) — mirrors the `ta1_acks` table
|
|
// and the `/api/ta1-acks` response. A TA1 is the lowest-level X12
|
|
// envelope ack: one per inbound ISA/IEA interchange, separate from
|
|
// the per-batch 999. Colorado Medicaid currently doesn't ship TA1s
|
|
// in the FromHPE path (the inbound MFT only carries 999s) but
|
|
// Gainwell has shipped them historically, so the UI shows them
|
|
// whenever one is on file.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* One persisted TA1 ACK row, camelCased for the UI. Re-shaped in
|
|
* `src/lib/api.ts` (`mapTa1Ack`) from the snake_case backend payload.
|
|
*
|
|
* ``ackCode`` is the interchange ack code: ``A`` = accepted,
|
|
* ``E`` = accepted with envelope errors, ``R`` = rejected.
|
|
* ``noteCode`` is an optional 3-digit X12 note code (e.g. ``000`` = no
|
|
* error, ``001`` = unsupported interchange version, etc.).
|
|
*/
|
|
export interface Ta1Ack {
|
|
id: number;
|
|
controlNumber: string;
|
|
ackCode: "A" | "E" | "R";
|
|
noteCode: string | null;
|
|
interchangeDate: string | null;
|
|
interchangeTime: string | null;
|
|
senderId: string | null;
|
|
receiverId: string | null;
|
|
sourceBatchId: string;
|
|
parsedAt: string | null;
|
|
/**
|
|
* SP28: originating `Batch` ids this TA1 envelope is linked to
|
|
* (per D4, TA1 is envelope-level so it links to `Batch` rather
|
|
* than `Claim`). Empty array means no auto-link resolved.
|
|
*/
|
|
linkedClaimIds?: string[];
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// SP28: claim↔ack link rows.
|
|
// Mirrors the `claim_acks` table (cyclone.db.ClaimAck) and the wire shape
|
|
// of `GET /api/claims/{id}/acks` + `GET /api/acks/{kind}/{id}/claims`.
|
|
// Per-AK2 granularity (D1): a single 999 with two AK2 set-responses
|
|
// produces two ClaimAck rows. TA1 batch-level rows have `claimId === null`
|
|
// — the `ClaimDrawer` panel filters those out; the `AckDrawer` panel
|
|
// renders them with the originating Batch instead of a claim.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Discriminator for the ack flavor that produced a `ClaimAck` row.
|
|
*
|
|
* - `"999"` — per-AK2 set-response (one row per AK2 in the inbound 999).
|
|
* - `"277ca"` — per-`ClaimStatus` (one row per `STC` in the inbound 277CA).
|
|
* - `"ta1"` — envelope-level (one row per TA1, no per-claim granularity).
|
|
*/
|
|
export type ClaimAckKind = "999" | "277ca" | "ta1";
|
|
|
|
/**
|
|
* One persisted claim↔ack link row, camelCased for the UI. The
|
|
* `claimId` is `null` for TA1 batch-level rows (D4) — the
|
|
* `ClaimDrawer` panel filters those out so the operator only sees
|
|
* per-claim acks; the `AckDrawer` panel renders them with the
|
|
* originating Batch instead.
|
|
*
|
|
* `claimState` is a derived join (the 999/277CA row carries the
|
|
* current `Claim.state` so the drawer can render the
|
|
* `ClaimStateBadge` inline without a second round-trip). For
|
|
* TA1 batch-level rows with `claimId === null`, this is `"n/a"`.
|
|
*
|
|
* `ak2Index` is populated only for `"999"` rows — 277CA and TA1
|
|
* don't have AK2 segments. `setControlNumber` is populated for
|
|
* 999/277CA rows (the value the upstream ack actually carried,
|
|
* regardless of which join path resolved the link — spec D10).
|
|
*/
|
|
export interface ClaimAck {
|
|
id: number;
|
|
claimId: string | null;
|
|
batchId: string | null;
|
|
ackId: number;
|
|
ackKind: ClaimAckKind;
|
|
ak2Index: number | null;
|
|
setControlNumber: string | null;
|
|
setAcceptRejectCode: string | null;
|
|
linkedAt: string;
|
|
linkedBy: "auto" | "manual";
|
|
claimState?: 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[];
|
|
/**
|
|
* SP28: compact form of the claim's `claim_acks` rows. Each entry
|
|
* has `ack_id`, `ack_kind`, `set_accept_reject_code`, `parsed_at`
|
|
* (no `claim_state` join — the drawer can use this for the
|
|
* initial panel render and rely on the live-tail for the full
|
|
* per-row payload via `useClaimAcks`). Empty array when the claim
|
|
* has no acks.
|
|
*/
|
|
ackLinks?: ClaimAck[];
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Auth (admin/user/viewer). Mirrors `users.to_public()` on the backend
|
|
// (backend/src/cyclone/auth/users.py). Role is the triple ("admin" |
|
|
// "user" | "viewer"); `disabledAt` is non-null when the account has been
|
|
// disabled by an admin. `createdAt` mirrors the original DB column.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export interface User {
|
|
id: number;
|
|
username: string;
|
|
role: "admin" | "user" | "viewer";
|
|
createdAt: string | null;
|
|
disabledAt?: string | null;
|
|
}
|