Skip to main content

datasynth_generators/industry/healthcare/
transactions.rs

1//! Healthcare transaction types.
2
3use chrono::NaiveDate;
4use rust_decimal::Decimal;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8use super::super::common::{IndustryGlAccount, IndustryJournalLine, IndustryTransaction};
9
10/// Payer type.
11#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
12pub enum PayerType {
13    /// Medicare (CMS).
14    Medicare,
15    /// Medicaid (state).
16    Medicaid,
17    /// Commercial insurance.
18    Commercial { carrier: String },
19    /// Self-pay patient.
20    SelfPay,
21    /// Workers compensation.
22    WorkersComp,
23    /// Tricare (military).
24    Tricare,
25    /// Veterans Affairs.
26    Va,
27}
28
29impl PayerType {
30    /// Returns the payer type code.
31    pub fn code(&self) -> &str {
32        match self {
33            PayerType::Medicare => "MCR",
34            PayerType::Medicaid => "MCD",
35            PayerType::Commercial { .. } => "COM",
36            PayerType::SelfPay => "SP",
37            PayerType::WorkersComp => "WC",
38            PayerType::Tricare => "TRC",
39            PayerType::Va => "VA",
40        }
41    }
42
43    /// Returns the expected reimbursement rate compared to charges.
44    pub fn expected_reimbursement_rate(&self) -> f64 {
45        match self {
46            PayerType::Medicare => 0.35,
47            PayerType::Medicaid => 0.25,
48            PayerType::Commercial { .. } => 0.55,
49            PayerType::SelfPay => 0.15,
50            PayerType::WorkersComp => 0.70,
51            PayerType::Tricare => 0.40,
52            PayerType::Va => 0.38,
53        }
54    }
55}
56
57/// Coding system types.
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
59pub enum CodingSystem {
60    /// ICD-10-CM diagnosis codes.
61    Icd10Cm,
62    /// ICD-10-PCS procedure codes.
63    Icd10Pcs,
64    /// CPT procedure codes.
65    Cpt,
66    /// HCPCS Level II codes.
67    Hcpcs,
68    /// DRG codes.
69    Drg,
70    /// Revenue codes.
71    RevCode,
72}
73
74impl CodingSystem {
75    /// Returns the code format description.
76    pub fn format_description(&self) -> &'static str {
77        match self {
78            CodingSystem::Icd10Cm => "A00-Z99.99",
79            CodingSystem::Icd10Pcs => "0-F16ABCD",
80            CodingSystem::Cpt => "00100-99499",
81            CodingSystem::Hcpcs => "A0000-V5999",
82            CodingSystem::Drg => "001-999",
83            CodingSystem::RevCode => "0001-0999",
84        }
85    }
86}
87
88/// Revenue cycle transactions.
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub enum RevenueCycleTransaction {
91    /// Patient registration.
92    PatientRegistration {
93        patient_id: String,
94        encounter_id: String,
95        payer: PayerType,
96        date: NaiveDate,
97    },
98    /// Charge capture.
99    ChargeCapture {
100        encounter_id: String,
101        charges: Vec<Charge>,
102        total_charges: Decimal,
103        date: NaiveDate,
104    },
105    /// Claim submission.
106    ClaimSubmission {
107        claim_id: String,
108        encounter_id: String,
109        payer: PayerType,
110        billed_amount: Decimal,
111        diagnosis_codes: Vec<String>,
112        procedure_codes: Vec<String>,
113        date: NaiveDate,
114    },
115    /// Payment posting.
116    PaymentPosting {
117        claim_id: String,
118        payer: PayerType,
119        payment_amount: Decimal,
120        adjustments: Vec<Adjustment>,
121        date: NaiveDate,
122    },
123    /// Denial management.
124    DenialPosting {
125        claim_id: String,
126        denial_reason: DenialReason,
127        denial_code: String,
128        denied_amount: Decimal,
129        date: NaiveDate,
130    },
131    /// Contractual adjustment.
132    ContractualAdjustment {
133        claim_id: String,
134        adjustment_amount: Decimal,
135        reason_code: String,
136        date: NaiveDate,
137    },
138    /// Patient responsibility posting.
139    PatientResponsibility {
140        claim_id: String,
141        patient_id: String,
142        responsibility_type: PatientResponsibilityType,
143        amount: Decimal,
144        date: NaiveDate,
145    },
146    /// Bad debt write-off.
147    BadDebtWriteOff {
148        patient_id: String,
149        amount: Decimal,
150        aging_days: u32,
151        date: NaiveDate,
152    },
153}
154
155/// A charge line.
156#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct Charge {
158    /// Charge ID.
159    pub charge_id: String,
160    /// CPT/HCPCS code.
161    pub procedure_code: String,
162    /// Revenue code.
163    pub revenue_code: String,
164    /// Service description.
165    pub description: String,
166    /// Quantity.
167    pub quantity: u32,
168    /// Unit charge amount.
169    pub unit_amount: Decimal,
170    /// Total charge.
171    pub total_amount: Decimal,
172    /// Service date.
173    pub service_date: NaiveDate,
174    /// Modifier codes.
175    pub modifiers: Vec<String>,
176}
177
178/// An adjustment line.
179#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct Adjustment {
181    /// Adjustment reason code.
182    pub reason_code: String,
183    /// Adjustment amount.
184    pub amount: Decimal,
185    /// Adjustment type.
186    pub adjustment_type: AdjustmentType,
187}
188
189/// Types of adjustments.
190#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
191pub enum AdjustmentType {
192    /// Contractual allowance.
193    Contractual,
194    /// Denial.
195    Denial,
196    /// Write-off.
197    WriteOff,
198    /// Bad debt.
199    BadDebt,
200    /// Charity care.
201    Charity,
202    /// Administrative.
203    Administrative,
204}
205
206/// Denial reason categories.
207#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
208pub enum DenialReason {
209    /// Medical necessity not established.
210    MedicalNecessity,
211    /// Prior authorization not obtained.
212    PriorAuthorization,
213    /// Coverage terminated.
214    CoverageTerminated,
215    /// Duplicate claim.
216    DuplicateClaim,
217    /// Invalid coding.
218    InvalidCoding,
219    /// Timely filing exceeded.
220    TimelyFiling,
221    /// Bundled service.
222    BundledService,
223    /// Coordination of benefits.
224    CoordinationOfBenefits,
225}
226
227/// Patient responsibility types.
228#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
229pub enum PatientResponsibilityType {
230    /// Copayment.
231    Copay,
232    /// Coinsurance.
233    Coinsurance,
234    /// Deductible.
235    Deductible,
236    /// Non-covered service.
237    NonCovered,
238}
239
240/// Clinical transactions.
241#[derive(Debug, Clone, Serialize, Deserialize)]
242pub enum ClinicalTransaction {
243    /// Procedure coding.
244    ProcedureCoding {
245        encounter_id: String,
246        cpt_codes: Vec<String>,
247        icd10_pcs_codes: Vec<String>,
248        date: NaiveDate,
249    },
250    /// Diagnosis coding.
251    DiagnosisCoding {
252        encounter_id: String,
253        icd10_codes: Vec<String>,
254        principal_diagnosis: String,
255        date: NaiveDate,
256    },
257    /// DRG assignment.
258    DrgAssignment {
259        encounter_id: String,
260        drg_code: String,
261        drg_weight: Decimal,
262        expected_reimbursement: Decimal,
263        date: NaiveDate,
264    },
265    /// Supply consumption.
266    SupplyConsumption {
267        encounter_id: String,
268        supplies: Vec<SupplyLine>,
269        total_cost: Decimal,
270        date: NaiveDate,
271    },
272    /// Pharmacy dispensing.
273    PharmacyDispensing {
274        encounter_id: String,
275        medications: Vec<MedicationLine>,
276        total_cost: Decimal,
277        date: NaiveDate,
278    },
279}
280
281/// Supply line item.
282#[derive(Debug, Clone, Serialize, Deserialize)]
283pub struct SupplyLine {
284    /// Supply item ID.
285    pub item_id: String,
286    /// Quantity used.
287    pub quantity: u32,
288    /// Unit cost.
289    pub unit_cost: Decimal,
290}
291
292/// Medication line item.
293#[derive(Debug, Clone, Serialize, Deserialize)]
294pub struct MedicationLine {
295    /// NDC code.
296    pub ndc: String,
297    /// Drug name.
298    pub drug_name: String,
299    /// Quantity.
300    pub quantity: Decimal,
301    /// Unit cost.
302    pub unit_cost: Decimal,
303}
304
305/// Union type for healthcare transactions.
306#[derive(Debug, Clone, Serialize, Deserialize)]
307pub enum HealthcareTransaction {
308    /// Revenue cycle transaction.
309    RevenueCycle(RevenueCycleTransaction),
310    /// Clinical transaction.
311    Clinical(ClinicalTransaction),
312}
313
314impl IndustryTransaction for HealthcareTransaction {
315    fn transaction_type(&self) -> &str {
316        match self {
317            HealthcareTransaction::RevenueCycle(rc) => match rc {
318                RevenueCycleTransaction::PatientRegistration { .. } => "patient_registration",
319                RevenueCycleTransaction::ChargeCapture { .. } => "charge_capture",
320                RevenueCycleTransaction::ClaimSubmission { .. } => "claim_submission",
321                RevenueCycleTransaction::PaymentPosting { .. } => "payment_posting",
322                RevenueCycleTransaction::DenialPosting { .. } => "denial_posting",
323                RevenueCycleTransaction::ContractualAdjustment { .. } => "contractual_adjustment",
324                RevenueCycleTransaction::PatientResponsibility { .. } => "patient_responsibility",
325                RevenueCycleTransaction::BadDebtWriteOff { .. } => "bad_debt_writeoff",
326            },
327            HealthcareTransaction::Clinical(clinical) => match clinical {
328                ClinicalTransaction::ProcedureCoding { .. } => "procedure_coding",
329                ClinicalTransaction::DiagnosisCoding { .. } => "diagnosis_coding",
330                ClinicalTransaction::DrgAssignment { .. } => "drg_assignment",
331                ClinicalTransaction::SupplyConsumption { .. } => "supply_consumption",
332                ClinicalTransaction::PharmacyDispensing { .. } => "pharmacy_dispensing",
333            },
334        }
335    }
336
337    fn date(&self) -> NaiveDate {
338        match self {
339            HealthcareTransaction::RevenueCycle(rc) => match rc {
340                RevenueCycleTransaction::PatientRegistration { date, .. }
341                | RevenueCycleTransaction::ChargeCapture { date, .. }
342                | RevenueCycleTransaction::ClaimSubmission { date, .. }
343                | RevenueCycleTransaction::PaymentPosting { date, .. }
344                | RevenueCycleTransaction::DenialPosting { date, .. }
345                | RevenueCycleTransaction::ContractualAdjustment { date, .. }
346                | RevenueCycleTransaction::PatientResponsibility { date, .. }
347                | RevenueCycleTransaction::BadDebtWriteOff { date, .. } => *date,
348            },
349            HealthcareTransaction::Clinical(clinical) => match clinical {
350                ClinicalTransaction::ProcedureCoding { date, .. }
351                | ClinicalTransaction::DiagnosisCoding { date, .. }
352                | ClinicalTransaction::DrgAssignment { date, .. }
353                | ClinicalTransaction::SupplyConsumption { date, .. }
354                | ClinicalTransaction::PharmacyDispensing { date, .. } => *date,
355            },
356        }
357    }
358
359    fn amount(&self) -> Option<Decimal> {
360        match self {
361            HealthcareTransaction::RevenueCycle(rc) => match rc {
362                RevenueCycleTransaction::ChargeCapture { total_charges, .. } => {
363                    Some(*total_charges)
364                }
365                RevenueCycleTransaction::ClaimSubmission { billed_amount, .. } => {
366                    Some(*billed_amount)
367                }
368                RevenueCycleTransaction::PaymentPosting { payment_amount, .. } => {
369                    Some(*payment_amount)
370                }
371                RevenueCycleTransaction::DenialPosting { denied_amount, .. } => {
372                    Some(*denied_amount)
373                }
374                RevenueCycleTransaction::ContractualAdjustment {
375                    adjustment_amount, ..
376                } => Some(*adjustment_amount),
377                RevenueCycleTransaction::PatientResponsibility { amount, .. } => Some(*amount),
378                RevenueCycleTransaction::BadDebtWriteOff { amount, .. } => Some(*amount),
379                _ => None,
380            },
381            HealthcareTransaction::Clinical(clinical) => match clinical {
382                ClinicalTransaction::DrgAssignment {
383                    expected_reimbursement,
384                    ..
385                } => Some(*expected_reimbursement),
386                ClinicalTransaction::SupplyConsumption { total_cost, .. } => Some(*total_cost),
387                ClinicalTransaction::PharmacyDispensing { total_cost, .. } => Some(*total_cost),
388                _ => None,
389            },
390        }
391    }
392
393    fn accounts(&self) -> Vec<String> {
394        match self {
395            HealthcareTransaction::RevenueCycle(rc) => match rc {
396                RevenueCycleTransaction::ChargeCapture { .. } => {
397                    vec!["1200".to_string(), "4100".to_string()]
398                }
399                RevenueCycleTransaction::PaymentPosting { .. } => {
400                    vec!["1000".to_string(), "1200".to_string()]
401                }
402                RevenueCycleTransaction::ContractualAdjustment { .. } => {
403                    vec!["4200".to_string(), "1200".to_string()]
404                }
405                RevenueCycleTransaction::BadDebtWriteOff { .. } => {
406                    vec!["6100".to_string(), "1200".to_string()]
407                }
408                _ => Vec::new(),
409            },
410            _ => Vec::new(),
411        }
412    }
413
414    fn to_journal_lines(&self) -> Vec<IndustryJournalLine> {
415        match self {
416            HealthcareTransaction::RevenueCycle(RevenueCycleTransaction::ChargeCapture {
417                total_charges,
418                ..
419            }) => {
420                vec![
421                    IndustryJournalLine::debit("1200", *total_charges, "Accounts Receivable"),
422                    IndustryJournalLine::credit("4100", *total_charges, "Patient Service Revenue"),
423                ]
424            }
425            HealthcareTransaction::RevenueCycle(RevenueCycleTransaction::PaymentPosting {
426                payment_amount,
427                ..
428            }) => {
429                vec![
430                    IndustryJournalLine::debit("1000", *payment_amount, "Cash"),
431                    IndustryJournalLine::credit("1200", *payment_amount, "Accounts Receivable"),
432                ]
433            }
434            HealthcareTransaction::RevenueCycle(
435                RevenueCycleTransaction::ContractualAdjustment {
436                    adjustment_amount, ..
437                },
438            ) => {
439                vec![
440                    IndustryJournalLine::debit("4200", *adjustment_amount, "Contractual Allowance"),
441                    IndustryJournalLine::credit("1200", *adjustment_amount, "Accounts Receivable"),
442                ]
443            }
444            HealthcareTransaction::RevenueCycle(RevenueCycleTransaction::BadDebtWriteOff {
445                amount,
446                ..
447            }) => {
448                vec![
449                    IndustryJournalLine::debit("6100", *amount, "Bad Debt Expense"),
450                    IndustryJournalLine::credit("1200", *amount, "Accounts Receivable"),
451                ]
452            }
453            _ => Vec::new(),
454        }
455    }
456
457    fn metadata(&self) -> HashMap<String, String> {
458        let mut meta = HashMap::new();
459        meta.insert("industry".to_string(), "healthcare".to_string());
460        meta.insert(
461            "transaction_type".to_string(),
462            self.transaction_type().to_string(),
463        );
464        meta
465    }
466}
467
468/// Generator for healthcare transactions.
469#[derive(Debug, Clone)]
470pub struct HealthcareTransactionGenerator {
471    /// Average encounters per day.
472    pub avg_daily_encounters: u32,
473    /// Denial rate (0.0-1.0).
474    pub denial_rate: f64,
475    /// Average charges per encounter.
476    pub avg_charges_per_encounter: u32,
477    /// Bad debt rate (0.0-1.0).
478    pub bad_debt_rate: f64,
479}
480
481impl Default for HealthcareTransactionGenerator {
482    fn default() -> Self {
483        Self {
484            avg_daily_encounters: 150,
485            denial_rate: 0.05,
486            avg_charges_per_encounter: 8,
487            bad_debt_rate: 0.03,
488        }
489    }
490}
491
492impl HealthcareTransactionGenerator {
493    /// Returns healthcare-specific GL accounts.
494    pub fn gl_accounts() -> Vec<IndustryGlAccount> {
495        vec![
496            IndustryGlAccount::new("1000", "Cash and Cash Equivalents", "Asset", "Cash")
497                .into_control(),
498            IndustryGlAccount::new(
499                "1200",
500                "Patient Accounts Receivable",
501                "Asset",
502                "Receivables",
503            )
504            .into_control(),
505            IndustryGlAccount::new(
506                "1210",
507                "Allowance for Doubtful Accounts",
508                "Asset",
509                "Receivables",
510            )
511            .with_normal_balance("Credit"),
512            IndustryGlAccount::new("4100", "Patient Service Revenue", "Revenue", "Revenue")
513                .with_normal_balance("Credit"),
514            IndustryGlAccount::new("4200", "Contractual Allowances", "Revenue", "Deductions"),
515            IndustryGlAccount::new("4210", "Charity Care", "Revenue", "Deductions"),
516            IndustryGlAccount::new("4220", "Bad Debt Provision", "Revenue", "Deductions"),
517            IndustryGlAccount::new("5100", "Salaries and Benefits", "Expense", "Labor"),
518            IndustryGlAccount::new("5200", "Medical Supplies", "Expense", "Supplies"),
519            IndustryGlAccount::new("5300", "Pharmaceuticals", "Expense", "Drugs"),
520            IndustryGlAccount::new("5400", "Professional Fees", "Expense", "Professional"),
521            IndustryGlAccount::new("6100", "Bad Debt Expense", "Expense", "Bad Debt"),
522        ]
523    }
524}
525
526#[cfg(test)]
527mod tests {
528    use super::*;
529
530    #[test]
531    fn test_payer_type() {
532        let medicare = PayerType::Medicare;
533        assert_eq!(medicare.code(), "MCR");
534        assert!(medicare.expected_reimbursement_rate() > 0.3);
535
536        let commercial = PayerType::Commercial {
537            carrier: "BlueCross".to_string(),
538        };
539        assert_eq!(commercial.code(), "COM");
540    }
541
542    #[test]
543    fn test_charge_capture() {
544        let tx = HealthcareTransaction::RevenueCycle(RevenueCycleTransaction::ChargeCapture {
545            encounter_id: "E001".to_string(),
546            charges: vec![Charge {
547                charge_id: "CHG001".to_string(),
548                procedure_code: "99213".to_string(),
549                revenue_code: "0510".to_string(),
550                description: "Office Visit".to_string(),
551                quantity: 1,
552                unit_amount: Decimal::new(150, 0),
553                total_amount: Decimal::new(150, 0),
554                service_date: NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
555                modifiers: Vec::new(),
556            }],
557            total_charges: Decimal::new(150, 0),
558            date: NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
559        });
560
561        assert_eq!(tx.transaction_type(), "charge_capture");
562        assert_eq!(tx.amount(), Some(Decimal::new(150, 0)));
563
564        let lines = tx.to_journal_lines();
565        assert_eq!(lines.len(), 2);
566    }
567
568    #[test]
569    fn test_payment_posting() {
570        let tx = HealthcareTransaction::RevenueCycle(RevenueCycleTransaction::PaymentPosting {
571            claim_id: "CLM001".to_string(),
572            payer: PayerType::Medicare,
573            payment_amount: Decimal::new(100, 0),
574            adjustments: vec![Adjustment {
575                reason_code: "CO-45".to_string(),
576                amount: Decimal::new(50, 0),
577                adjustment_type: AdjustmentType::Contractual,
578            }],
579            date: NaiveDate::from_ymd_opt(2024, 1, 20).unwrap(),
580        });
581
582        assert_eq!(tx.transaction_type(), "payment_posting");
583    }
584
585    #[test]
586    fn test_gl_accounts() {
587        let accounts = HealthcareTransactionGenerator::gl_accounts();
588        assert!(accounts.len() >= 10);
589
590        let ar = accounts.iter().find(|a| a.account_number == "1200");
591        assert!(ar.is_some());
592    }
593}