medicafe 0.250820.7__py3-none-any.whl → 0.250822.1__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.
- MediBot/MediBot.py +16 -0
- MediBot/MediBot_Charges.py +133 -0
- MediBot/MediBot_UI.py +42 -1
- MediBot/__init__.py +1 -1
- MediBot/update_medicafe.py +66 -24
- MediCafe/__init__.py +1 -1
- MediCafe/api_core.py +164 -7
- MediCafe/graphql_utils.py +66 -1
- MediCafe/smart_import.py +2 -1
- MediLink/MediLink_Charges.py +517 -0
- MediLink/MediLink_Deductible.py +91 -45
- MediLink/MediLink_Deductible_Validator.py +40 -3
- MediLink/__init__.py +1 -1
- {medicafe-0.250820.7.dist-info → medicafe-0.250822.1.dist-info}/METADATA +1 -1
- {medicafe-0.250820.7.dist-info → medicafe-0.250822.1.dist-info}/RECORD +19 -18
- {medicafe-0.250820.7.dist-info → medicafe-0.250822.1.dist-info}/LICENSE +0 -0
- {medicafe-0.250820.7.dist-info → medicafe-0.250822.1.dist-info}/WHEEL +0 -0
- {medicafe-0.250820.7.dist-info → medicafe-0.250822.1.dist-info}/entry_points.txt +0 -0
- {medicafe-0.250820.7.dist-info → medicafe-0.250822.1.dist-info}/top_level.txt +0 -0
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")
|