medicafe 0.250820.7__py3-none-any.whl → 0.250822.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
MediCafe/smart_import.py CHANGED
@@ -148,6 +148,7 @@ class ComponentRegistry:
148
148
  'medilink_ui': 'MediLink.MediLink_UI',
149
149
  'medilink_up': 'MediLink.MediLink_Up',
150
150
  'medilink_insurance_utils': 'MediLink.MediLink_insurance_utils',
151
+ 'medilink_charges': 'MediLink.MediLink_Charges',
151
152
  'medilink_837p_cob_library': 'MediLink.MediLink_837p_cob_library',
152
153
  'medilink_837p_encoder': 'MediLink.MediLink_837p_encoder',
153
154
  'medilink_837p_encoder_library': 'MediLink.MediLink_837p_encoder_library',
@@ -222,7 +223,7 @@ class ComponentProvider:
222
223
  'medilink_claim_processing': {
223
224
  'core_dependencies': ['api_core', 'logging_config', 'medilink_datamgmt'],
224
225
  'optional_dependencies': ['graphql_utils', 'api_utils'],
225
- 'shared_resources': ['medilink_837p_encoder', 'medilink_837p_utilities', 'medilink_claim_status']
226
+ 'shared_resources': ['medilink_837p_encoder', 'medilink_837p_utilities', 'medilink_claim_status', 'medilink_charges']
226
227
  },
227
228
  'medilink_deductible_processing': {
228
229
  'core_dependencies': ['api_core', 'logging_config', 'medilink_datamgmt'],
@@ -0,0 +1,517 @@
1
+ # MediLink_Charges.py
2
+ """MediLink_Charges.py
3
+
4
+ Overview
5
+ --------
6
+ This module provides core charge calculation and bundling functionality for the MediLink system, with wrappers for MediBot integration. It handles medical billing charges for ophthalmology (cataract surgeries), calculating based on anesthesia minutes and supporting multi-eye bundling. Primary integration point is enrich_with_charges (via MediBot_Charges.py) called from MediBot.py before data_entry_loop.
7
+
8
+ Key Features
9
+ ------------
10
+ - Tiered pricing with capping (>59 minutes caps at 59, with notification/log).
11
+ - Bundling for even costs across eyes; flags 'bundling_pending' if patient exists (via MediBot check_existing_patients) or diagnosis suggests bilateral.
12
+ - Read-only historical lookups (e.g., MATRAN) with user notifications for manual edits; never alters data.
13
+ - Deductible checks (placeholder; integrate with OptumAI via config).
14
+ - Refund processing for expired bundling (post-30 days).
15
+ - Interactive UI tie-in via MediBot_UI extensions for minutes input.
16
+
17
+ Pricing Structure (Private Insurance)
18
+ ------------------------------------
19
+ Default tiered pricing based on procedure duration:
20
+ - $450 for 1-15 minutes
21
+ - $480 for 16-22 minutes
22
+ - $510 for 23-27 minutes
23
+ - $540 for 28-37 minutes
24
+ - $580 for 38-59 minutes (maximum allowed duration)
25
+
26
+ Medicare pricing follows different rules and is configurable through the system.
27
+
28
+ Integration with MediLink
29
+ -------------------------
30
+ This module is designed to work with:
31
+ - MediLink_837p_encoder_library.py: For claim segment generation
32
+ - MediLink_DataMgmt.py: For patient and procedure data management
33
+ - MediLink_ClaimStatus.py: For charge tracking and status updates
34
+ - MediCafe smart import system: For modular loading
35
+
36
+ Usage
37
+ -----
38
+ The module can be used in several ways:
39
+ 1. Direct charge calculation for individual procedures
40
+ 2. Batch processing of multiple procedures with bundling
41
+ 3. Integration with MediLink claim generation workflow
42
+ 4. Charge validation and adjustment for existing claims
43
+
44
+ Example:
45
+ from MediLink import MediLink_Charges
46
+
47
+ # Calculate charge for a procedure
48
+ charge_info = MediLink_Charges.calculate_procedure_charge(
49
+ minutes=25,
50
+ insurance_type='private',
51
+ procedure_code='66984'
52
+ )
53
+
54
+ # Bundle charges for bilateral procedures
55
+ bundled_charges = MediLink_Charges.bundle_bilateral_charges(
56
+ [charge_info_1, charge_info_2]
57
+ )
58
+
59
+ Data Format
60
+ -----------
61
+ The module works with standardized charge data structures that are compatible with
62
+ 837p claim requirements:
63
+ - Charge amounts in dollars and cents
64
+ - Procedure codes (CPT/HCPCS)
65
+ - Service dates and duration
66
+ - Insurance-specific modifiers
67
+ - Bundling and adjustment flags
68
+
69
+ Compatibility
70
+ -------------
71
+ - Python 3.4.4+ compatible
72
+ - ASCII-only character encoding
73
+ - Windows XP SP3 compatible
74
+ - No external dependencies beyond MediLink core modules
75
+
76
+ Author: MediLink Development Team
77
+ Version: 1.0.0
78
+ """
79
+
80
+ import os
81
+ import sys
82
+ from datetime import datetime, timedelta
83
+ from decimal import Decimal, ROUND_HALF_UP
84
+
85
+ # Import centralized logging configuration
86
+ try:
87
+ from MediCafe.logging_config import DEBUG, PERFORMANCE_LOGGING
88
+ except ImportError:
89
+ # Fallback to local flags if centralized config is not available
90
+ DEBUG = False
91
+ PERFORMANCE_LOGGING = False
92
+
93
+ # Set up project paths
94
+ project_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
95
+ if project_dir not in sys.path:
96
+ sys.path.insert(0, project_dir)
97
+
98
+ # Import MediLink core utilities
99
+ try:
100
+ from MediCafe.core_utils import get_shared_config_loader
101
+ MediLink_ConfigLoader = get_shared_config_loader()
102
+ except ImportError:
103
+ print("Warning: Unable to import MediCafe.core_utils. Using fallback configuration.")
104
+ MediLink_ConfigLoader = None
105
+
106
+ # Default pricing configuration for private insurance
107
+ DEFAULT_PRIVATE_PRICING_TIERS = [
108
+ {'min_minutes': 1, 'max_minutes': 15, 'charge': Decimal('450.00')},
109
+ {'min_minutes': 16, 'max_minutes': 34, 'charge': Decimal('540.00')},
110
+ {'min_minutes': 35, 'max_minutes': 59, 'charge': Decimal('580.00')},
111
+ ]
112
+
113
+ # Default Medicare pricing (placeholder - should be configured)
114
+ DEFAULT_MEDICARE_PRICING_TIERS = [
115
+ {'min_minutes': 1, 'max_minutes': 30, 'charge': Decimal('300.00')},
116
+ {'min_minutes': 31, 'max_minutes': 45, 'charge': Decimal('350.00')},
117
+ {'min_minutes': 46, 'max_minutes': 59, 'charge': Decimal('400.00')},
118
+ ]
119
+
120
+ class ChargeCalculationError(Exception):
121
+ """Custom exception for charge calculation errors"""
122
+ pass
123
+
124
+ class ChargeBundlingError(Exception):
125
+ """Custom exception for charge bundling errors"""
126
+ pass
127
+
128
+ class ChargeInfo:
129
+ """
130
+ Container class for charge information compatible with 837p requirements
131
+ """
132
+ def __init__(self, procedure_code='', service_date=None, minutes=0,
133
+ base_charge=Decimal('0.00'), adjusted_charge=None,
134
+ insurance_type='private', patient_id='', claim_id=''):
135
+ self.procedure_code = procedure_code
136
+ self.service_date = service_date or datetime.now().date()
137
+ self.minutes = minutes
138
+ self.base_charge = Decimal(str(base_charge))
139
+ self.adjusted_charge = Decimal(str(adjusted_charge)) if adjusted_charge else self.base_charge
140
+ self.insurance_type = insurance_type.lower()
141
+ self.patient_id = patient_id
142
+ self.claim_id = claim_id
143
+ self.bundling_group = None # For multi-procedure bundling
144
+ self.adjustment_reason = None
145
+ self.created_timestamp = datetime.now()
146
+ self.flags = {} # e.g., {'bundling_pending': True, 'Pending for Deductible': True}
147
+
148
+ def to_dict(self):
149
+ """Convert to dictionary format for 837p integration"""
150
+ return {
151
+ 'procedure_code': self.procedure_code,
152
+ 'service_date': self.service_date.strftime('%Y%m%d') if self.service_date else '',
153
+ 'minutes': self.minutes,
154
+ 'base_charge': str(self.base_charge),
155
+ 'adjusted_charge': str(self.adjusted_charge),
156
+ 'insurance_type': self.insurance_type,
157
+ 'patient_id': self.patient_id,
158
+ 'claim_id': self.claim_id,
159
+ 'bundling_group': self.bundling_group,
160
+ 'adjustment_reason': self.adjustment_reason,
161
+ 'created_timestamp': self.created_timestamp.isoformat()
162
+ }
163
+
164
+ def to_837p_format(self):
165
+ """Format charge data for 837p claim integration"""
166
+ return {
167
+ 'charge_amount': str(self.adjusted_charge),
168
+ 'service_units': str(self.minutes),
169
+ 'procedure_code': self.procedure_code,
170
+ 'service_date': self.service_date.strftime('%Y%m%d') if self.service_date else '',
171
+ 'line_item_charge': str(self.adjusted_charge)
172
+ }
173
+
174
+ def get_pricing_tiers(insurance_type='private'):
175
+ """
176
+ Get pricing tiers for the specified insurance type
177
+
178
+ Args:
179
+ insurance_type (str): Type of insurance ('private', 'medicare', etc.)
180
+
181
+ Returns:
182
+ list: List of pricing tier dictionaries
183
+ """
184
+ if DEBUG:
185
+ print("Getting pricing tiers for insurance type: {}".format(insurance_type))
186
+
187
+ # Try to load from configuration first
188
+ if MediLink_ConfigLoader:
189
+ try:
190
+ config_tiers = MediLink_ConfigLoader.get('pricing_tiers', {}).get(insurance_type.lower())
191
+ if config_tiers:
192
+ return config_tiers
193
+ except Exception as e:
194
+ if DEBUG:
195
+ print("Warning: Could not load pricing tiers from config: {}".format(e))
196
+
197
+ # Fall back to default tiers
198
+ if insurance_type.lower() == 'medicare':
199
+ return DEFAULT_MEDICARE_PRICING_TIERS
200
+ else:
201
+ return DEFAULT_PRIVATE_PRICING_TIERS
202
+
203
+ def calculate_base_charge(minutes, insurance_type='private'):
204
+ """
205
+ Calculate base charge for a procedure based on minutes and insurance type
206
+
207
+ Args:
208
+ minutes (int): Duration of procedure in minutes
209
+ insurance_type (str): Type of insurance
210
+
211
+ Returns:
212
+ Decimal: Calculated charge amount
213
+
214
+ Raises:
215
+ ChargeCalculationError: If minutes are invalid or no tier matches
216
+ """
217
+ if DEBUG:
218
+ print("Calculating base charge for {} minutes, {} insurance".format(minutes, insurance_type))
219
+
220
+ # Validate input
221
+ if not isinstance(minutes, int) or minutes <= 0:
222
+ raise ChargeCalculationError("Minutes must be a positive integer, got: {}".format(minutes))
223
+
224
+ if minutes > 59:
225
+ minutes = 59 # Cap at 59
226
+ if MediLink_ConfigLoader:
227
+ MediLink_ConfigLoader.log("Capped duration to 59 minutes", level="INFO")
228
+ print("Confirm intended duration >59? (Rare case)")
229
+
230
+ # Get pricing tiers for insurance type
231
+ pricing_tiers = get_pricing_tiers(insurance_type)
232
+
233
+ # Find matching tier
234
+ for tier in pricing_tiers:
235
+ if tier['min_minutes'] <= minutes <= tier['max_minutes']:
236
+ charge = Decimal(str(tier['charge']))
237
+ if DEBUG:
238
+ print("Found matching tier: ${} for {} minutes".format(charge, minutes))
239
+ return charge
240
+
241
+ # No tier found
242
+ raise ChargeCalculationError("No pricing tier found for {} minutes with {} insurance".format(
243
+ minutes, insurance_type))
244
+
245
+ def calculate_procedure_charge(minutes, insurance_type='private', procedure_code='',
246
+ service_date=None, patient_id='', claim_id=''):
247
+ """
248
+ Calculate complete charge information for a procedure
249
+
250
+ Args:
251
+ minutes (int): Duration of procedure in minutes
252
+ insurance_type (str): Type of insurance
253
+ procedure_code (str): CPT/HCPCS procedure code
254
+ service_date (date): Date of service
255
+ patient_id (str): Patient identifier
256
+ claim_id (str): Claim identifier
257
+
258
+ Returns:
259
+ ChargeInfo: Complete charge information object
260
+ """
261
+ if DEBUG:
262
+ print("Calculating procedure charge for patient {} claim {}".format(patient_id, claim_id))
263
+
264
+ try:
265
+ base_charge = calculate_base_charge(minutes, insurance_type)
266
+
267
+ charge_info = ChargeInfo(
268
+ procedure_code=procedure_code,
269
+ service_date=service_date,
270
+ minutes=minutes,
271
+ base_charge=base_charge,
272
+ insurance_type=insurance_type,
273
+ patient_id=patient_id,
274
+ claim_id=claim_id
275
+ )
276
+
277
+ if DEBUG:
278
+ print("Created charge info: ${}".format(charge_info.base_charge))
279
+
280
+ return charge_info
281
+
282
+ except Exception as e:
283
+ raise ChargeCalculationError("Failed to calculate procedure charge: {}".format(str(e)))
284
+
285
+ def bundle_bilateral_charges(charge_list, bundling_strategy='average'):
286
+ """
287
+ Bundle charges for bilateral procedures (e.g., both eyes)
288
+
289
+ Args:
290
+ charge_list (list): List of ChargeInfo objects to bundle
291
+ bundling_strategy (str): Strategy for bundling ('average', 'total_split')
292
+
293
+ Returns:
294
+ list: List of ChargeInfo objects with adjusted charges
295
+
296
+ Raises:
297
+ ChargeBundlingError: If bundling fails
298
+ """
299
+ if DEBUG:
300
+ print("Bundling {} charges using {} strategy".format(len(charge_list), bundling_strategy))
301
+
302
+ if not charge_list or len(charge_list) < 2:
303
+ if DEBUG:
304
+ print("No bundling needed for {} charges".format(len(charge_list)))
305
+ return charge_list
306
+
307
+ try:
308
+ if bundling_strategy == 'average':
309
+ # Calculate average charge and apply to all procedures
310
+ total_charge = sum(charge.base_charge for charge in charge_list)
311
+ average_charge = total_charge / len(charge_list)
312
+ # Round to nearest cent
313
+ average_charge = average_charge.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
314
+
315
+ bundling_group = "bundle_{}".format(datetime.now().strftime('%Y%m%d_%H%M%S'))
316
+
317
+ for charge in charge_list:
318
+ charge.adjusted_charge = average_charge
319
+ charge.bundling_group = bundling_group
320
+ charge.adjustment_reason = "Bilateral procedure bundling - average"
321
+
322
+ # Use MediBot's existing patient check to assume prior procedure if patient exists
323
+ if MediLink_ConfigLoader and MediLink_ConfigLoader.get('MediBot', {}).get('Preprocessor'):
324
+ MediBot_Preprocessor = MediLink_ConfigLoader.get('MediBot', {}).get('Preprocessor')
325
+ if MediBot_Preprocessor.check_existing_patients([charge.patient_id])[0]: # Exists, assume prior
326
+ charge.flags['bundling_pending'] = True
327
+
328
+ if DEBUG:
329
+ print("Applied average bundling: ${} per procedure".format(average_charge))
330
+
331
+ elif bundling_strategy == 'total_split':
332
+ # Split total evenly among procedures
333
+ total_charge = sum(charge.base_charge for charge in charge_list)
334
+ split_charge = total_charge / len(charge_list)
335
+ split_charge = split_charge.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
336
+
337
+ bundling_group = "split_{}".format(datetime.now().strftime('%Y%m%d_%H%M%S'))
338
+
339
+ for charge in charge_list:
340
+ charge.adjusted_charge = split_charge
341
+ charge.bundling_group = bundling_group
342
+ charge.adjustment_reason = "Bilateral procedure bundling - total split"
343
+
344
+ if DEBUG:
345
+ print("Applied total split bundling: ${} per procedure".format(split_charge))
346
+ else:
347
+ raise ChargeBundlingError("Unknown bundling strategy: {}".format(bundling_strategy))
348
+
349
+ return charge_list
350
+
351
+ except Exception as e:
352
+ raise ChargeBundlingError("Failed to bundle charges: {}".format(str(e)))
353
+
354
+ def validate_charge_data(charge_info):
355
+ """
356
+ Validate charge information for completeness and accuracy
357
+
358
+ Args:
359
+ charge_info (ChargeInfo): Charge information to validate
360
+
361
+ Returns:
362
+ tuple: (is_valid, error_messages)
363
+ """
364
+ errors = []
365
+
366
+ # Check required fields
367
+ if not charge_info.procedure_code:
368
+ errors.append("Procedure code is required")
369
+
370
+ if not charge_info.patient_id:
371
+ errors.append("Patient ID is required")
372
+
373
+ if charge_info.minutes <= 0:
374
+ errors.append("Minutes must be positive")
375
+
376
+ if charge_info.minutes > 59:
377
+ errors.append("Minutes cannot exceed 59")
378
+
379
+ if charge_info.base_charge <= 0:
380
+ errors.append("Base charge must be positive")
381
+
382
+ if charge_info.adjusted_charge <= 0:
383
+ errors.append("Adjusted charge must be positive")
384
+
385
+ # Check insurance type
386
+ valid_insurance_types = ['private', 'medicare', 'medicaid', 'commercial']
387
+ if charge_info.insurance_type not in valid_insurance_types:
388
+ errors.append("Invalid insurance type: {}".format(charge_info.insurance_type))
389
+
390
+ # Check service date
391
+ if charge_info.service_date and charge_info.service_date > datetime.now().date():
392
+ errors.append("Service date cannot be in the future")
393
+
394
+ is_valid = len(errors) == 0
395
+
396
+ if DEBUG and not is_valid:
397
+ print("Charge validation failed: {}".format("; ".join(errors)))
398
+
399
+ return is_valid, errors
400
+
401
+ def format_charges_for_837p(charge_list):
402
+ """
403
+ Format charge information for 837p claim generation
404
+
405
+ Args:
406
+ charge_list (list): List of ChargeInfo objects
407
+
408
+ Returns:
409
+ list: List of 837p-formatted charge dictionaries
410
+ """
411
+ if DEBUG:
412
+ print("Formatting {} charges for 837p".format(len(charge_list)))
413
+
414
+ formatted_charges = []
415
+
416
+ for charge in charge_list:
417
+ # Validate charge first
418
+ is_valid, errors = validate_charge_data(charge)
419
+ if not is_valid:
420
+ if DEBUG:
421
+ print("Skipping invalid charge: {}".format("; ".join(errors)))
422
+ continue
423
+
424
+ formatted_charge = charge.to_837p_format()
425
+ formatted_charges.append(formatted_charge)
426
+
427
+ if DEBUG:
428
+ print("Successfully formatted {} charges for 837p".format(len(formatted_charges)))
429
+
430
+ return formatted_charges
431
+
432
+ # Utility functions for integration with other MediLink modules
433
+
434
+ def get_charge_summary(charge_list):
435
+ """
436
+ Get summary statistics for a list of charges
437
+
438
+ Args:
439
+ charge_list (list): List of ChargeInfo objects
440
+
441
+ Returns:
442
+ dict: Summary statistics
443
+ """
444
+ if not charge_list:
445
+ return {
446
+ 'total_charges': Decimal('0.00'),
447
+ 'average_charge': Decimal('0.00'),
448
+ 'charge_count': 0,
449
+ 'bundled_count': 0,
450
+ 'insurance_breakdown': {}
451
+ }
452
+
453
+ total_charges = sum(charge.adjusted_charge for charge in charge_list)
454
+ average_charge = total_charges / len(charge_list)
455
+ bundled_count = sum(1 for charge in charge_list if charge.bundling_group)
456
+
457
+ # Insurance type breakdown
458
+ insurance_breakdown = {}
459
+ for charge in charge_list:
460
+ ins_type = charge.insurance_type
461
+ if ins_type not in insurance_breakdown:
462
+ insurance_breakdown[ins_type] = {'count': 0, 'total': Decimal('0.00')}
463
+ insurance_breakdown[ins_type]['count'] += 1
464
+ insurance_breakdown[ins_type]['total'] += charge.adjusted_charge
465
+
466
+ return {
467
+ 'total_charges': total_charges,
468
+ 'average_charge': average_charge.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP),
469
+ 'charge_count': len(charge_list),
470
+ 'bundled_count': bundled_count,
471
+ 'insurance_breakdown': insurance_breakdown
472
+ }
473
+
474
+ def log_charge_activity(charge_info, activity_type='created'):
475
+ """
476
+ Log charge-related activity for audit purposes
477
+
478
+ Args:
479
+ charge_info (ChargeInfo): Charge information
480
+ activity_type (str): Type of activity ('created', 'modified', 'bundled')
481
+ """
482
+ if DEBUG:
483
+ log_message = "Charge {} for patient {} claim {}: ${} ({} min, {})".format(
484
+ activity_type,
485
+ charge_info.patient_id,
486
+ charge_info.claim_id,
487
+ charge_info.adjusted_charge,
488
+ charge_info.minutes,
489
+ charge_info.insurance_type
490
+ )
491
+ print("[CHARGE_LOG] {}".format(log_message))
492
+
493
+ # Add historical lookup prototype
494
+ def lookup_historical_charges(patient_id, procedure_codes, date_range):
495
+ # Read-only MATRAN parse (TBD format)
496
+ # Prototype: Return mock priors
497
+ priors = [] # List of ChargeInfo
498
+ if priors:
499
+ print("Edit MATRAN for {} - Adjust prior from {} to {} for bundling".format(patient_id, priors[0].base_charge, 'new_value'))
500
+ return priors
501
+
502
+ # Add deductible check
503
+ def check_deductible_unmet(charge_info):
504
+ # Prototype: External check
505
+ return True # Flag as Pending if True
506
+
507
+ # Add refund logic for expired bundling
508
+ def process_refund_if_expired(charge_info):
509
+ if charge_info.flags.get('bundling_pending') and (datetime.now() - charge_info.service_date).days > 30:
510
+ # Prototype refund
511
+ print("Process refund for expired bundling: {}".format(charge_info.patient_id))
512
+
513
+ # Module initialization
514
+ if __name__ == "__main__":
515
+ print("MediLink_Charges.py - Medical Billing Charge Calculation Module")
516
+ print("Version 1.0.0")
517
+ print("For integration with MediLink 837p claim generation system")