1use chrono::NaiveDate;
4use rust_decimal::Decimal;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8use super::super::common::{IndustryGlAccount, IndustryJournalLine, IndustryTransaction};
9
10#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
12pub enum PayerType {
13 Medicare,
15 Medicaid,
17 Commercial { carrier: String },
19 SelfPay,
21 WorkersComp,
23 Tricare,
25 Va,
27}
28
29impl PayerType {
30 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
59pub enum CodingSystem {
60 Icd10Cm,
62 Icd10Pcs,
64 Cpt,
66 Hcpcs,
68 Drg,
70 RevCode,
72}
73
74impl CodingSystem {
75 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#[derive(Debug, Clone, Serialize, Deserialize)]
90pub enum RevenueCycleTransaction {
91 PatientRegistration {
93 patient_id: String,
94 encounter_id: String,
95 payer: PayerType,
96 date: NaiveDate,
97 },
98 ChargeCapture {
100 encounter_id: String,
101 charges: Vec<Charge>,
102 total_charges: Decimal,
103 date: NaiveDate,
104 },
105 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 PaymentPosting {
117 claim_id: String,
118 payer: PayerType,
119 payment_amount: Decimal,
120 adjustments: Vec<Adjustment>,
121 date: NaiveDate,
122 },
123 DenialPosting {
125 claim_id: String,
126 denial_reason: DenialReason,
127 denial_code: String,
128 denied_amount: Decimal,
129 date: NaiveDate,
130 },
131 ContractualAdjustment {
133 claim_id: String,
134 adjustment_amount: Decimal,
135 reason_code: String,
136 date: NaiveDate,
137 },
138 PatientResponsibility {
140 claim_id: String,
141 patient_id: String,
142 responsibility_type: PatientResponsibilityType,
143 amount: Decimal,
144 date: NaiveDate,
145 },
146 BadDebtWriteOff {
148 patient_id: String,
149 amount: Decimal,
150 aging_days: u32,
151 date: NaiveDate,
152 },
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct Charge {
158 pub charge_id: String,
160 pub procedure_code: String,
162 pub revenue_code: String,
164 pub description: String,
166 pub quantity: u32,
168 pub unit_amount: Decimal,
170 pub total_amount: Decimal,
172 pub service_date: NaiveDate,
174 pub modifiers: Vec<String>,
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct Adjustment {
181 pub reason_code: String,
183 pub amount: Decimal,
185 pub adjustment_type: AdjustmentType,
187}
188
189#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
191pub enum AdjustmentType {
192 Contractual,
194 Denial,
196 WriteOff,
198 BadDebt,
200 Charity,
202 Administrative,
204}
205
206#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
208pub enum DenialReason {
209 MedicalNecessity,
211 PriorAuthorization,
213 CoverageTerminated,
215 DuplicateClaim,
217 InvalidCoding,
219 TimelyFiling,
221 BundledService,
223 CoordinationOfBenefits,
225}
226
227#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
229pub enum PatientResponsibilityType {
230 Copay,
232 Coinsurance,
234 Deductible,
236 NonCovered,
238}
239
240#[derive(Debug, Clone, Serialize, Deserialize)]
242pub enum ClinicalTransaction {
243 ProcedureCoding {
245 encounter_id: String,
246 cpt_codes: Vec<String>,
247 icd10_pcs_codes: Vec<String>,
248 date: NaiveDate,
249 },
250 DiagnosisCoding {
252 encounter_id: String,
253 icd10_codes: Vec<String>,
254 principal_diagnosis: String,
255 date: NaiveDate,
256 },
257 DrgAssignment {
259 encounter_id: String,
260 drg_code: String,
261 drg_weight: Decimal,
262 expected_reimbursement: Decimal,
263 date: NaiveDate,
264 },
265 SupplyConsumption {
267 encounter_id: String,
268 supplies: Vec<SupplyLine>,
269 total_cost: Decimal,
270 date: NaiveDate,
271 },
272 PharmacyDispensing {
274 encounter_id: String,
275 medications: Vec<MedicationLine>,
276 total_cost: Decimal,
277 date: NaiveDate,
278 },
279}
280
281#[derive(Debug, Clone, Serialize, Deserialize)]
283pub struct SupplyLine {
284 pub item_id: String,
286 pub quantity: u32,
288 pub unit_cost: Decimal,
290}
291
292#[derive(Debug, Clone, Serialize, Deserialize)]
294pub struct MedicationLine {
295 pub ndc: String,
297 pub drug_name: String,
299 pub quantity: Decimal,
301 pub unit_cost: Decimal,
303}
304
305#[derive(Debug, Clone, Serialize, Deserialize)]
307pub enum HealthcareTransaction {
308 RevenueCycle(RevenueCycleTransaction),
310 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#[derive(Debug, Clone)]
470pub struct HealthcareTransactionGenerator {
471 pub avg_daily_encounters: u32,
473 pub denial_rate: f64,
475 pub avg_charges_per_encounter: u32,
477 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 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}