source: backend/src/main/java/medora/service/BillingService.java

Last change on this file was 48d2bed, checked in by MBK <marija.karapandzova@…>, 45 hours ago

Remote database setup, fixed models and application properties

  • Property mode set to 100644
File size: 20.5 KB
Line 
1package medora.service;
2
3import medora.models.domain.Billing;
4import medora.models.domain.MedicalRecord;
5import medora.models.domain.Admin;
6import medora.models.domain.BillingLabTests;
7import medora.models.domain.BillingProcedures;
8import medora.models.domain.PerformedProcedures;
9import medora.models.domain.PerformedLabTests;
10import medora.models.enums.PaymentStatus;
11import medora.repository.BillingRepository;
12import medora.repository.MedicalRecordRepository;
13import medora.repository.AdminRepository;
14import medora.repository.BillingLabTestsRepository;
15import medora.repository.BillingProceduresRepository;
16import medora.repository.PerformedProcedureRepository;
17import medora.repository.PerformedLabTestRepository;
18import medora.repository.PatientRepository;
19import medora.dto.BillingDetailDTO;
20import medora.dto.BillingItemDTO;
21import org.slf4j.Logger;
22import org.slf4j.LoggerFactory;
23import org.springframework.stereotype.Service;
24import org.springframework.transaction.annotation.Transactional;
25
26import java.math.BigDecimal;
27import java.time.LocalDate;
28import java.util.List;
29import java.util.Optional;
30import java.util.ArrayList;
31
32/**
33 * BillingService handles billing operations.
34 * UC020 – Generate Billing Record
35 * UC021 – Record Payment Status
36 * UC022 – View Billing History
37 */
38@Service
39public class BillingService {
40
41 private static final Logger logger = LoggerFactory.getLogger(BillingService.class);
42
43 private final BillingRepository billingRepository;
44 private final MedicalRecordRepository medicalRecordRepository;
45 private final AdminRepository adminRepository;
46 private final BillingLabTestsRepository billingLabTestsRepository;
47 private final BillingProceduresRepository billingProceduresRepository;
48 private final PerformedProcedureRepository performedProcedureRepository;
49 private final PerformedLabTestRepository performedLabTestRepository;
50 private final PatientRepository patientRepository;
51
52 public BillingService(BillingRepository billingRepository,
53 MedicalRecordRepository medicalRecordRepository,
54 AdminRepository adminRepository,
55 BillingLabTestsRepository billingLabTestsRepository,
56 BillingProceduresRepository billingProceduresRepository,
57 PerformedProcedureRepository performedProcedureRepository,
58 PerformedLabTestRepository performedLabTestRepository,
59 PatientRepository patientRepository) {
60 this.billingRepository = billingRepository;
61 this.medicalRecordRepository = medicalRecordRepository;
62 this.adminRepository = adminRepository;
63 this.billingLabTestsRepository = billingLabTestsRepository;
64 this.billingProceduresRepository = billingProceduresRepository;
65 this.performedProcedureRepository = performedProcedureRepository;
66 this.performedLabTestRepository = performedLabTestRepository;
67 this.patientRepository = patientRepository;
68 }
69
70 /**
71 * UC020 – Generate Billing Record
72 * Create a billing record based on procedures and lab tests
73 */
74 @Transactional
75 public Billing generateBillingRecord(Long medicalRecordId, Long adminId, BigDecimal totalCost) {
76 if (medicalRecordId == null || medicalRecordId <= 0) {
77 throw new IllegalArgumentException("Medical record ID must be valid");
78 }
79 if (adminId == null || adminId <= 0) {
80 throw new IllegalArgumentException("Admin ID must be valid");
81 }
82 if (totalCost == null || totalCost.compareTo(BigDecimal.ZERO) < 0) {
83 throw new IllegalArgumentException("Total cost must be valid");
84 }
85
86 MedicalRecord medicalRecord = medicalRecordRepository.findById(medicalRecordId)
87 .orElseThrow(() -> new RuntimeException("Medical record not found with ID: " + medicalRecordId));
88
89 Admin admin = adminRepository.findById(adminId)
90 .orElseThrow(() -> new RuntimeException("Admin not found with ID: " + adminId));
91
92 Billing billing = new Billing();
93 billing.setBillId(billingRepository.findMaxBillId() + 1);
94 billing.setMedicalRecord(medicalRecord);
95 billing.setTotalCost(totalCost);
96 billing.setAdmin(admin);
97 billing.setPaymentStatus(PaymentStatus.PENDING);
98 return billingRepository.save(billing);
99 }
100
101 /**
102 * UC021 – Record Payment Status
103 * Update billing payment status
104 */
105 @Transactional
106 public Billing updatePaymentStatus(Long billId, PaymentStatus paymentStatus, LocalDate paymentDate) {
107 if (billId == null || billId <= 0) {
108 throw new IllegalArgumentException("Bill ID must be valid");
109 }
110 if (paymentStatus == null) {
111 throw new IllegalArgumentException("Payment status is required");
112 }
113
114 Billing billing = billingRepository.findById(billId)
115 .orElseThrow(() -> new RuntimeException("Billing record not found with ID: " + billId));
116
117 billing.setPaymentStatus(paymentStatus);
118 if (paymentDate != null && paymentStatus == PaymentStatus.PAID) {
119 billing.setPaymentDate(paymentDate);
120 }
121
122 logger.info("Updating payment status for bill ID: {} to {}", billId, paymentStatus);
123 return billingRepository.save(billing);
124 }
125
126 /**
127 * UC022 – View Billing History
128 * Get all billing records for a patient (via medical record)
129 */
130 @Transactional(readOnly = true)
131 public List<Billing> getBillingHistoryForPatient(Long patientId) {
132 if (patientId == null || patientId <= 0) {
133 throw new IllegalArgumentException("Patient ID must be valid");
134 }
135
136 logger.info("Fetching billing history for patient ID: {}", patientId);
137 return billingRepository.findBillingHistoryForPatient(patientId);
138 }
139
140 /**
141 * UC022 – View Billing History
142 * Get billing record by ID
143 */
144 @Transactional(readOnly = true)
145 public Optional<Billing> getBillingById(Long billId) {
146 if (billId == null || billId <= 0) {
147 throw new IllegalArgumentException("Bill ID must be valid");
148 }
149 logger.info("Fetching billing record with ID: {}", billId);
150 return billingRepository.findById(billId);
151 }
152
153 /**
154 * Get all billing records
155 */
156 @Transactional(readOnly = true)
157 public List<Billing> getAllBillingRecords() {
158 logger.info("Fetching all billing records");
159 return billingRepository.findAll();
160 }
161
162 /**
163 * Get billing records by payment status
164 */
165 @Transactional(readOnly = true)
166 public List<Billing> getBillingByPaymentStatus(PaymentStatus paymentStatus) {
167 if (paymentStatus == null) {
168 throw new IllegalArgumentException("Payment status is required");
169 }
170
171 logger.info("Fetching billing records with payment status: {}", paymentStatus);
172 return billingRepository.findByPaymentStatus(paymentStatus.toString());
173 }
174
175 /**
176 * Get billing record for a medical record
177 */
178 @Transactional(readOnly = true)
179 public Optional<Billing> getBillingForMedicalRecord(Long medicalRecordId) {
180 if (medicalRecordId == null || medicalRecordId <= 0) {
181 throw new IllegalArgumentException("Medical record ID must be valid");
182 }
183
184 if (!medicalRecordRepository.existsById(medicalRecordId)) {
185 throw new RuntimeException("Medical record not found with ID: " + medicalRecordId);
186 }
187
188 logger.info("Fetching billing record for medical record ID: {}", medicalRecordId);
189 // Get all bills and filter by medical record
190 return billingRepository.findAll()
191 .stream()
192 .filter(b -> b.getMedicalRecord().getRecordId().equals(medicalRecordId))
193 .findFirst();
194 }
195
196 /**
197 * Calculate total cost from procedures and lab tests for a medical record
198 */
199 @Transactional(readOnly = true)
200 public BigDecimal calculateTotalCostForBilling(Long billId) {
201 if (billId == null || billId <= 0) {
202 throw new IllegalArgumentException("Bill ID must be valid");
203 }
204
205 if (!billingRepository.existsById(billId)) {
206 throw new RuntimeException("Billing record not found with ID: " + billId);
207 }
208
209 // Calculate cost from procedures
210 BigDecimal procedureCost = billingProceduresRepository.calculateTotalCostForBilling(billId);
211 if (procedureCost == null) {
212 procedureCost = BigDecimal.ZERO;
213 }
214
215 // Calculate cost from lab tests
216 BigDecimal labTestCost = billingLabTestsRepository.calculateTotalCostForBilling(billId);
217 if (labTestCost == null) {
218 labTestCost = BigDecimal.ZERO;
219 }
220
221 logger.info("Total billing cost for bill ID {}: procedures={}, lab tests={}",
222 billId, procedureCost, labTestCost);
223 return procedureCost.add(labTestCost);
224 }
225
226 /**
227 * Add a procedure to a billing record
228 */
229 @Transactional
230 public BillingProcedures addProcedureToBilling(Long billId, Long procedureId) {
231 if (billId == null || billId <= 0) {
232 throw new IllegalArgumentException("Bill ID must be valid");
233 }
234 if (procedureId == null || procedureId <= 0) {
235 throw new IllegalArgumentException("Procedure ID must be valid");
236 }
237
238 Billing billing = billingRepository.findById(billId)
239 .orElseThrow(() -> new RuntimeException("Billing record not found with ID: " + billId));
240
241 // Note: You'll need to inject ProcedureRepository to get the procedure
242 // This is a placeholder - adjust based on your actual Procedure entity
243 logger.info("Adding procedure {} to billing record {}", procedureId, billId);
244
245 return null; // Will be implemented with ProcedureRepository injection
246 }
247
248 /**
249 * Add a lab test to a billing record
250 */
251 @Transactional
252 public BillingLabTests addLabTestToBilling(Long billId, Long testId) {
253 if (billId == null || billId <= 0) {
254 throw new IllegalArgumentException("Bill ID must be valid");
255 }
256 if (testId == null || testId <= 0) {
257 throw new IllegalArgumentException("Test ID must be valid");
258 }
259
260 Billing billing = billingRepository.findById(billId)
261 .orElseThrow(() -> new RuntimeException("Billing record not found with ID: " + billId));
262
263 // Note: You'll need to inject LabTestRepository to get the test
264 // This is a placeholder - adjust based on your actual LabTests entity
265 logger.info("Adding lab test {} to billing record {}", testId, billId);
266
267 return null; // Will be implemented with LabTestRepository injection
268 }
269
270 /**
271 * UC020 – Auto-generate billing when a procedure or lab test is performed
272 * Creates a billing record if one doesn't exist for the patient on that date
273 * Calculates total cost from all procedures and lab tests performed that day
274 */
275 @Transactional
276 public void autoGenerateBillingForPatientService(Long patientId, LocalDate serviceDate) {
277 try {
278 if (patientId == null || patientId <= 0) {
279 throw new IllegalArgumentException("Patient ID must be valid");
280 }
281 if (serviceDate == null) {
282 throw new IllegalArgumentException("Service date must be valid");
283 }
284
285 logger.info("Starting auto-billing for patient {} on date {}", patientId, serviceDate);
286
287 // Get patient's medical record (or create one if it doesn't exist)
288 MedicalRecord medicalRecord = medicalRecordRepository.findByPatientPatientId(patientId)
289 .orElseGet(() -> {
290 logger.info("Creating new medical record for patient {}", patientId);
291 MedicalRecord newRecord = new MedicalRecord();
292 newRecord.setRecordId(medicalRecordRepository.findMaxRecordId() + 1);
293 newRecord.setPatient(patientRepository.findById(patientId)
294 .orElseThrow(() -> new RuntimeException("Patient not found with ID: " + patientId)));
295 return medicalRecordRepository.save(newRecord);
296 });
297
298 logger.info("Using medical record {} for patient {}", medicalRecord.getRecordId(), patientId);
299
300 // Get all procedures and lab tests for the patient on that date
301 List<PerformedProcedures> procedures = performedProcedureRepository.findByPatientAndDate(patientId, serviceDate);
302 List<PerformedLabTests> labTests = performedLabTestRepository.findByPatientAndDate(patientId, serviceDate);
303
304 logger.info("Found {} procedures and {} lab tests for patient {} on {}",
305 procedures.size(), labTests.size(), patientId, serviceDate);
306
307 // Only generate billing if there are procedures or lab tests on that date
308 if (procedures.isEmpty() && labTests.isEmpty()) {
309 logger.info("No procedures or lab tests found for patient {} on {}", patientId, serviceDate);
310 return;
311 }
312
313 // Calculate total cost
314 BigDecimal procedureCost = procedures.stream()
315 .map(p -> {
316 BigDecimal cost = p.getProcedure().getCost();
317 logger.debug("Procedure {} cost: {}", p.getProcedure().getProcedureId(), cost);
318 return cost;
319 })
320 .reduce(BigDecimal.ZERO, BigDecimal::add);
321
322 BigDecimal labTestCost = labTests.stream()
323 .map(lt -> {
324 BigDecimal cost = lt.getLabTest().getCost();
325 logger.debug("Lab test {} cost: {}", lt.getLabTest().getTestId(), cost);
326 return cost;
327 })
328 .reduce(BigDecimal.ZERO, BigDecimal::add);
329
330 BigDecimal totalCost = procedureCost.add(labTestCost);
331 logger.info("Total cost calculation: procedures={}, labTests={}, total={}", procedureCost, labTestCost, totalCost);
332
333 // Get default admin (first admin in system) - skip billing if none found
334 Optional<Admin> adminOptional = adminRepository.findAll()
335 .stream()
336 .findFirst();
337
338 if (adminOptional.isEmpty()) {
339 logger.warn("No admin found in system - skipping automatic billing generation for patient {} on {}", patientId, serviceDate);
340 return;
341 }
342
343 Admin admin = adminOptional.get();
344
345 // Check if billing already exists for this patient on this date
346 Billing billing = billingRepository.findBillingForPatientOnDate(patientId, serviceDate);
347
348 if (billing != null) {
349 logger.info("Billing record {} already exists for patient {} on {}, updating with new total",
350 billing.getBillId(), patientId, serviceDate);
351 billing.setTotalCost(totalCost);
352 } else {
353 // Create new billing record
354 billing = new Billing();
355 billing.setBillId(billingRepository.findMaxBillId() + 1);
356 billing.setMedicalRecord(medicalRecord);
357 billing.setAdmin(admin);
358 billing.setTotalCost(totalCost);
359 billing.setPaymentStatus(PaymentStatus.PENDING);
360 billing.setPaymentDate(serviceDate);
361
362 logger.info("Creating new billing record for patient {} on {}", patientId, serviceDate);
363 }
364
365 Billing savedBilling = billingRepository.save(billing);
366 logger.info("Billing record {} for patient {} on {} with total cost: {}",
367 savedBilling.getBillId(), patientId, serviceDate, totalCost);
368
369 // Link procedures to billing (only if not already linked)
370 for (PerformedProcedures procedure : procedures) {
371 if (!billingProceduresRepository.existsByBillingBillIdAndProcedureProcedureId(
372 savedBilling.getBillId(), procedure.getProcedure().getProcedureId())) {
373 BillingProcedures billingProcedure = new BillingProcedures(savedBilling, procedure.getProcedure());
374 billingProceduresRepository.save(billingProcedure);
375 logger.debug("Linked procedure {} to billing {}", procedure.getProcedure().getProcedureId(), savedBilling.getBillId());
376 }
377 }
378
379 // Link lab tests to billing (only if not already linked)
380 for (PerformedLabTests labTest : labTests) {
381 if (!billingLabTestsRepository.existsByBillingBillIdAndLabTestTestId(
382 savedBilling.getBillId(), labTest.getLabTest().getTestId())) {
383 BillingLabTests billingLabTest = new BillingLabTests(savedBilling, labTest.getLabTest());
384 billingLabTestsRepository.save(billingLabTest);
385 logger.debug("Linked lab test {} to billing {}", labTest.getLabTest().getTestId(), savedBilling.getBillId());
386 }
387 }
388
389 logger.info("Successfully processed {} procedures and {} lab tests for billing record {}",
390 procedures.size(), labTests.size(), savedBilling.getBillId());
391
392 } catch (Exception e) {
393 logger.error("Error in auto-billing for patient {} on date {}: {}", patientId, serviceDate, e.getMessage(), e);
394 throw e;
395 }
396 }
397
398 /**
399 * Get detailed billing information with itemized procedures and lab tests
400 */
401 @Transactional(readOnly = true)
402 public BillingDetailDTO getBillingDetail(Long billId) {
403 if (billId == null || billId <= 0) {
404 throw new IllegalArgumentException("Bill ID must be valid");
405 }
406
407 Billing billing = billingRepository.findById(billId)
408 .orElseThrow(() -> new RuntimeException("Billing record not found with ID: " + billId));
409
410 // Get procedures for this bill
411 List<Object[]> procedureResults = billingRepository.findProceduresForBilling(billId);
412 List<BillingItemDTO> procedures = new ArrayList<>();
413 for (Object[] row : procedureResults) {
414 procedures.add(new BillingItemDTO(
415 ((Number) row[0]).longValue(),
416 (String) row[1],
417 (BigDecimal) row[2]
418 ));
419 }
420
421 // Get lab tests for this bill
422 List<Object[]> labTestResults = billingRepository.findLabTestsForBilling(billId);
423 List<BillingItemDTO> labTests = new ArrayList<>();
424 for (Object[] row : labTestResults) {
425 labTests.add(new BillingItemDTO(
426 ((Number) row[0]).longValue(),
427 (String) row[1],
428 (BigDecimal) row[2]
429 ));
430 }
431
432 // Build the detail DTO
433 BillingDetailDTO detail = new BillingDetailDTO();
434 detail.setBillId(billing.getBillId());
435 detail.setPatientId(billing.getMedicalRecord().getPatient().getPatientId());
436 detail.setPatientName(billing.getMedicalRecord().getPatient().getFirstName() + " " +
437 billing.getMedicalRecord().getPatient().getLastName());
438 detail.setPatientEmbg(billing.getMedicalRecord().getPatient().getEmbg());
439 detail.setPatientPhone(billing.getMedicalRecord().getPatient().getPhoneNumber());
440 detail.setTotalCost(billing.getTotalCost());
441 detail.setPaymentStatus(billing.getPaymentStatus().toString());
442 detail.setPaymentDate(billing.getPaymentDate());
443 detail.setBillDate(billing.getPaymentDate());
444 detail.setProcedures(procedures);
445 detail.setLabTests(labTests);
446
447 logger.info("Retrieved detailed billing information for bill {}", billId);
448 return detail;
449 }
450
451 @Transactional
452 public long deleteTestRecords(Long maxBillIdToKeep) {
453 try {
454 // Use native SQL to delete all billing data including audit logs
455 Long billingCount = (long) billingRepository.findAll().size();
456
457 // Delete billing procedures and lab tests (they reference billing)
458 billingProceduresRepository.deleteAll();
459 billingLabTestsRepository.deleteAll();
460
461 // Delete billing records
462 billingRepository.deleteAll();
463
464 logger.info("✅ Deleted {} billing records successfully", billingCount);
465 return billingCount;
466 } catch (Exception e) {
467 logger.error("❌ Error deleting billing records: {}", e.getMessage());
468 e.printStackTrace();
469 throw e;
470 }
471 }
472}
Note: See TracBrowser for help on using the repository browser.