From b0d8439fdb68edfcb97995bc125d5883567b542f Mon Sep 17 00:00:00 2001 From: mutajonathan Date: Thu, 13 Nov 2025 09:59:11 +0300 Subject: [PATCH 01/20] Add provider and location attributes in the BillableService --- .../billing/api/model/BillableService.java | 22 +++++++++++++ .../api/search/BillableServiceSearch.java | 6 ++++ api/src/main/resources/Bill.hbm.xml | 2 ++ .../resource/BillableServiceResource.java | 32 +++++++++++++++++++ .../restmapper/BillableServiceMapper.java | 21 ++++++++++++ omod/src/main/resources/liquibase.xml | 17 ++++++++++ 6 files changed, 100 insertions(+) diff --git a/api/src/main/java/org/openmrs/module/billing/api/model/BillableService.java b/api/src/main/java/org/openmrs/module/billing/api/model/BillableService.java index 27e89323..f8098ca6 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/model/BillableService.java +++ b/api/src/main/java/org/openmrs/module/billing/api/model/BillableService.java @@ -18,6 +18,8 @@ import org.openmrs.BaseOpenmrsData; import org.openmrs.Concept; +import org.openmrs.Location; +import org.openmrs.Provider; public class BillableService extends BaseOpenmrsData { @@ -39,6 +41,10 @@ public class BillableService extends BaseOpenmrsData { private BillableServiceStatus serviceStatus = BillableServiceStatus.ENABLED; + private Provider provider; + + private Location location; + public int getBillableServiceId() { return billableServiceId; } @@ -125,4 +131,20 @@ public void addServicePrice(CashierItemPrice price) { this.servicePrices.add(price); price.setBillableService(this); } + + public Provider getProvider() { + return provider; + } + + public void setProvider(Provider provider) { + this.provider = provider; + } + + public Location getLocation() { + return location; + } + + public void setLocation(Location location) { + this.location = location; + } } diff --git a/api/src/main/java/org/openmrs/module/billing/api/search/BillableServiceSearch.java b/api/src/main/java/org/openmrs/module/billing/api/search/BillableServiceSearch.java index 65020548..7891d066 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/search/BillableServiceSearch.java +++ b/api/src/main/java/org/openmrs/module/billing/api/search/BillableServiceSearch.java @@ -53,5 +53,11 @@ public void updateCriteria(Criteria criteria) { if (billableService.getName() != null) { criteria.add(Restrictions.like("name", billableService.getName(), MatchMode.ANYWHERE)); } + if (billableService.getProvider() != null) { + criteria.add(Restrictions.eq("provider", billableService.getProvider())); + } + if (billableService.getLocation() != null) { + criteria.add(Restrictions.eq("location", billableService.getLocation())); + } } } diff --git a/api/src/main/resources/Bill.hbm.xml b/api/src/main/resources/Bill.hbm.xml index 6c66da5d..6414d37a 100644 --- a/api/src/main/resources/Bill.hbm.xml +++ b/api/src/main/resources/Bill.hbm.xml @@ -51,6 +51,8 @@ + + diff --git a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillableServiceResource.java b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillableServiceResource.java index 9766aa4a..7ae91672 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillableServiceResource.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillableServiceResource.java @@ -70,6 +70,8 @@ protected AlreadyPaged doSearch(RequestContext context) { context.getParameter("serviceCategory")) : null; String serviceStatus = context.getParameter("isDisabled"); String serviceName = context.getParameter("serviceName"); + String locationUuid = context.getParameter("location"); + String providerUuid = context.getParameter("provider"); BillableServiceStatus status = BillableServiceStatus.ENABLED; if (Strings.isNotEmpty(serviceStatus)) { if (serviceStatus.equalsIgnoreCase("yes") || serviceStatus.equalsIgnoreCase("1")) { @@ -82,6 +84,14 @@ protected AlreadyPaged doSearch(RequestContext context) { searchTemplate.setServiceStatus(status); searchTemplate.setName(serviceName); + if (Strings.isNotEmpty(locationUuid)) { + searchTemplate.setLocation(Context.getLocationService().getLocationByUuid(locationUuid)); + } + + if (Strings.isNotEmpty(providerUuid)) { + searchTemplate.setProvider(Context.getProviderService().getProviderByUuid(providerUuid)); + } + IBillableItemsService service = Context.getService(IBillableItemsService.class); return new AlreadyPaged<>(context, service.findServices(new BillableServiceSearch(searchTemplate, false)), false); } @@ -97,6 +107,8 @@ public DelegatingResourceDescription getRepresentationDescription(Representation description.addProperty("serviceCategory"); description.addProperty("servicePrices"); description.addProperty("serviceStatus"); + description.addProperty("provider"); + description.addProperty("location"); } else if (rep instanceof CustomRepresentation) { //For custom representation, must be null // - let the user decide which properties should be included in the response @@ -121,6 +133,26 @@ public void setServicePrices(BillableService instance, List it } } + @PropertySetter("provider") + public void setProvider(BillableService instance, Object value) { + if (value != null) { + String uuid = value.toString(); + instance.setProvider(Context.getProviderService().getProviderByUuid(uuid)); + } else { + instance.setProvider(null); + } + } + + @PropertySetter("location") + public void setLocation(BillableService instance, Object value) { + if (value != null) { + String uuid = value.toString(); + instance.setLocation(Context.getLocationService().getLocationByUuid(uuid)); + } else { + instance.setLocation(null); + } + } + @Override public DelegatingResourceDescription getCreatableProperties() { return getRepresentationDescription(new DefaultRepresentation()); diff --git a/omod/src/main/java/org/openmrs/module/billing/web/rest/restmapper/BillableServiceMapper.java b/omod/src/main/java/org/openmrs/module/billing/web/rest/restmapper/BillableServiceMapper.java index 10bb185b..4a1b9516 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/rest/restmapper/BillableServiceMapper.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/rest/restmapper/BillableServiceMapper.java @@ -28,6 +28,8 @@ public class BillableServiceMapper { private String concept; private String serviceType; private String serviceCategory; + private String provider; + private String location; private List servicePrices; private BillableServiceStatus serviceStatus = BillableServiceStatus.ENABLED; @@ -87,6 +89,23 @@ public void setConcept(String concept) { this.concept = concept; } + + public String getLocation() { + return location; + } + + public void setLocation(String location) { + this.location = location; + } + + public String getProvider() { + return provider; + } + + public void setProvider(String provider) { + this.provider = provider; + } + public BillableService billableServiceMapper(BillableServiceMapper mapper) { BillableService service = new BillableService(); List servicePrices = new ArrayList<>(); @@ -96,6 +115,8 @@ public BillableService billableServiceMapper(BillableServiceMapper mapper) { service.setServiceType(Context.getConceptService().getConceptByUuid(mapper.getServiceType())); service.setServiceCategory(Context.getConceptService().getConceptByUuid(mapper.getServiceCategory())); service.setServiceStatus(mapper.getServiceStatus()); + service.setProvider(Context.getProviderService().getProviderByUuid(mapper.getProvider())); + service.setLocation(Context.getLocationService().getLocationByUuid(mapper.getLocation())); for (CashierItemPriceMapper itemPrice : mapper.getServicePrices()) { CashierItemPrice price = new CashierItemPrice(); price.setName(itemPrice.getName()); diff --git a/omod/src/main/resources/liquibase.xml b/omod/src/main/resources/liquibase.xml index ca038697..a278c2b2 100644 --- a/omod/src/main/resources/liquibase.xml +++ b/omod/src/main/resources/liquibase.xml @@ -945,4 +945,21 @@ property = 'cashier.receipt.logoPath' + + + + + + + + + + + + + \ No newline at end of file From e3f71f80836bc8c1c8d938b2f4c7e07c36dc0a63 Mon Sep 17 00:00:00 2001 From: Mutagubya Jonathan Date: Mon, 17 Nov 2025 17:11:24 +0300 Subject: [PATCH 02/20] Merge pull request #2 from Muta-Jonathan/(feat)Add-is_default-Column-to-Payment-Mode-and-Cash-Point (feat)Add is_default Column to Payment Mode and Cash Point Tables & Expose Field in REST Resource --- .../module/billing/api/model/CashPoint.java | 10 ++++++++++ .../module/billing/api/model/PaymentMode.java | 10 ++++++++++ api/src/main/resources/Bill.hbm.xml | 1 + api/src/main/resources/Cashier.hbm.xml | 1 + .../web/rest/resource/CashPointResource.java | 10 ++++++++++ .../web/rest/resource/PaymentModeResource.java | 8 ++++++++ omod/src/main/resources/liquibase.xml | 15 +++++++++++++++ 7 files changed, 55 insertions(+) diff --git a/api/src/main/java/org/openmrs/module/billing/api/model/CashPoint.java b/api/src/main/java/org/openmrs/module/billing/api/model/CashPoint.java index 268a4af9..a72143d8 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/model/CashPoint.java +++ b/api/src/main/java/org/openmrs/module/billing/api/model/CashPoint.java @@ -27,6 +27,8 @@ public class CashPoint extends BaseOpenmrsMetadata { private Location location; + private boolean isDefault = false; + public Location getLocation() { return location; } @@ -35,6 +37,14 @@ public void setLocation(Location location) { this.location = location; } + public boolean getIsDefault() { + return isDefault; + } + + public void setIsDefault(boolean isDefault) { + this.isDefault = isDefault; + } + @Override public Integer getId() { return this.cashPointId; diff --git a/api/src/main/java/org/openmrs/module/billing/api/model/PaymentMode.java b/api/src/main/java/org/openmrs/module/billing/api/model/PaymentMode.java index 7d6c97e9..c7094318 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/model/PaymentMode.java +++ b/api/src/main/java/org/openmrs/module/billing/api/model/PaymentMode.java @@ -24,6 +24,8 @@ public class PaymentMode extends BaseInstanceCustomizableType + diff --git a/api/src/main/resources/Cashier.hbm.xml b/api/src/main/resources/Cashier.hbm.xml index 3d1c3524..96d9064e 100644 --- a/api/src/main/resources/Cashier.hbm.xml +++ b/api/src/main/resources/Cashier.hbm.xml @@ -15,6 +15,7 @@ + diff --git a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/CashPointResource.java b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/CashPointResource.java index 07a87895..9b2bdf5f 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/CashPointResource.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/CashPointResource.java @@ -19,6 +19,7 @@ import org.openmrs.module.billing.api.base.entity.IMetadataDataService; import org.openmrs.module.billing.api.model.CashPoint; import org.openmrs.module.webservices.rest.web.RestConstants; +import org.openmrs.module.webservices.rest.web.annotation.PropertySetter; import org.openmrs.module.webservices.rest.web.annotation.Resource; import org.openmrs.module.webservices.rest.web.representation.Representation; import org.openmrs.module.webservices.rest.web.resource.impl.DelegatingResourceDescription; @@ -33,6 +34,7 @@ public class CashPointResource extends BaseRestMetadataResource { public DelegatingResourceDescription getRepresentationDescription(Representation rep) { DelegatingResourceDescription description = super.getRepresentationDescription(rep); description.addProperty("location", Representation.REF); + description.addProperty("isDefault"); return description; } @@ -40,6 +42,7 @@ public DelegatingResourceDescription getRepresentationDescription(Representation public DelegatingResourceDescription getCreatableProperties() { DelegatingResourceDescription description = super.getCreatableProperties(); description.addProperty("location"); + description.addProperty("isDefault"); return description; } @@ -52,4 +55,11 @@ public CashPoint newDelegate() { public Class> getServiceClass() { return ICashPointService.class; } + + @PropertySetter("isDefault") + public void setIsDefault(CashPoint instance, Boolean isDefault) { + if (isDefault != null) { + instance.setIsDefault(isDefault); + } + } } diff --git a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/PaymentModeResource.java b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/PaymentModeResource.java index eb59c464..09ce3fda 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/PaymentModeResource.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/PaymentModeResource.java @@ -57,6 +57,7 @@ public DelegatingResourceDescription getRepresentationDescription(Representation DelegatingResourceDescription description = super.getRepresentationDescription(rep); if (!(rep instanceof RefRepresentation)) { description.addProperty("sortOrder"); + description.addProperty("isDefault"); } else if (rep instanceof CustomRepresentation) { //For custom representation, must be null // - let the user decide which properties should be included in the response @@ -70,4 +71,11 @@ public DelegatingResourceDescription getRepresentationDescription(Representation public void setAttributeTypes(PaymentMode instance, List attributeTypes) { super.baseSetAttributeTypes(instance, attributeTypes); } + + @PropertySetter("isDefault") + public void setIsDefault(PaymentMode instance, Boolean isDefault) { + if (isDefault != null) { + instance.setIsDefault(isDefault); + } + } } diff --git a/omod/src/main/resources/liquibase.xml b/omod/src/main/resources/liquibase.xml index a278c2b2..8e4a99a8 100644 --- a/omod/src/main/resources/liquibase.xml +++ b/omod/src/main/resources/liquibase.xml @@ -962,4 +962,19 @@ baseColumnNames="provider_id" referencedTableName="provider" referencedColumnNames="provider_id" constraintName="fk_billable_service_provider"/> + + Adding is_default column to both cashier_payment_mode and cashier_cash_point + + + + + + + + + + + + + \ No newline at end of file From ec37c181f8c54fe0c9b2fe985a28126830b9b34b Mon Sep 17 00:00:00 2001 From: Mutagubya Jonathan Date: Fri, 12 Dec 2025 09:00:07 +0300 Subject: [PATCH 03/20] (feat) Disable Bill auto creation on Drug Orders (#3) --- .../billing/advice/GenerateBillFromOrderAdvice.java | 8 ++++++++ .../billing/advice/OrderCreationMethodBeforeAdvice.java | 8 ++++++++ omod/src/main/resources/config.xml | 5 ++++- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/api/src/main/java/org/openmrs/module/billing/advice/GenerateBillFromOrderAdvice.java b/api/src/main/java/org/openmrs/module/billing/advice/GenerateBillFromOrderAdvice.java index 46e07341..597771cb 100644 --- a/api/src/main/java/org/openmrs/module/billing/advice/GenerateBillFromOrderAdvice.java +++ b/api/src/main/java/org/openmrs/module/billing/advice/GenerateBillFromOrderAdvice.java @@ -44,6 +44,8 @@ public class GenerateBillFromOrderAdvice implements AfterReturningAdvice { private static final Log LOG = LogFactory.getLog(GenerateBillFromOrderAdvice.class); + private static final String DISABLE_DRUG_ORDER_BILL_AUTO_CREATION = "billing.disableDrugOrderBillAutoCreation"; + OrderService orderService = Context.getOrderService(); IBillService billService = Context.getService(IBillService.class); @@ -75,6 +77,12 @@ public void afterReturning(Object returnValue, Method method, Object[] args, Obj String cashierUUID = Context.getAuthenticatedUser().getUuid(); if (order instanceof DrugOrder) { + // Check if drug order bill autocreation is disabled + boolean disableAutoBillCreation = Boolean.parseBoolean( + Context.getAdministrationService().getGlobalProperty(DISABLE_DRUG_ORDER_BILL_AUTO_CREATION)); + if (disableAutoBillCreation) { + return; // Skip drug order bill processing + } DrugOrder drugOrder = (DrugOrder) order; Integer drugID = drugOrder.getDrug() != null ? drugOrder.getDrug().getDrugId() : 0; double drugQuantity = drugOrder.getQuantity() != null ? drugOrder.getQuantity() : 0.0; diff --git a/api/src/main/java/org/openmrs/module/billing/advice/OrderCreationMethodBeforeAdvice.java b/api/src/main/java/org/openmrs/module/billing/advice/OrderCreationMethodBeforeAdvice.java index 9fe6c78d..4030df12 100644 --- a/api/src/main/java/org/openmrs/module/billing/advice/OrderCreationMethodBeforeAdvice.java +++ b/api/src/main/java/org/openmrs/module/billing/advice/OrderCreationMethodBeforeAdvice.java @@ -53,6 +53,8 @@ public class OrderCreationMethodBeforeAdvice implements MethodBeforeAdvice { private static final Log LOG = LogFactory.getLog(OrderCreationMethodBeforeAdvice.class); + private static final String DISABLE_DRUG_ORDER_BILL_AUTO_CREATION = "billing.disableDrugOrderBillAutoCreation"; + OrderService orderService = Context.getOrderService(); IBillService billService = Context.getService(IBillService.class); @@ -79,6 +81,12 @@ public void before(Method method, Object[] args, Object target) throws Throwable Patient patient = order.getPatient(); String cashierUUID = Context.getAuthenticatedUser().getUuid(); if (order instanceof DrugOrder) { + // Check if drug order bill autocreation is disabled + boolean disableAutoBillCreation = Boolean.parseBoolean( + Context.getAdministrationService().getGlobalProperty(DISABLE_DRUG_ORDER_BILL_AUTO_CREATION)); + if (disableAutoBillCreation) { + return; // Skip drug order bill processing + } DrugOrder drugOrder = (DrugOrder) order; Integer drugID = drugOrder.getDrug() != null ? drugOrder.getDrug().getDrugId() : 0; double drugQuantity = drugOrder.getQuantity() != null ? drugOrder.getQuantity() : 0.0; diff --git a/omod/src/main/resources/config.xml b/omod/src/main/resources/config.xml index 00624e2e..238c39fb 100644 --- a/omod/src/main/resources/config.xml +++ b/omod/src/main/resources/config.xml @@ -153,7 +153,10 @@ - + + ${project.parent.artifactId}.disableDrugOrderBillAutoCreation + Disable automatic bill creation for drug orders + From 1852b2fb30a074daa4039cb4e556a6d961f02f29 Mon Sep 17 00:00:00 2001 From: Mutagubya Jonathan Date: Fri, 12 Dec 2025 13:33:16 +0300 Subject: [PATCH 04/20] Merge remote-tracking branch upstream/main (#4) * BillableService and CashierItemPrice should be metadata (#49) * BillabeService and CashierItemPrice are metadata and should be treated as such * Fixes from code review * Remove unused imports across project * Enhance unit tests for service implementations (#52) * Revert "Allow bills to be viewed or download in the browser (#50)" This reverts commit d0e9d3b194a7ea96570c26b1eda2d28a00f2b45d. * (feat) O3-5197: Enable custom REST representation support for Bill Resource (#54) * (fix) O3-5122: Bill should not return voided line items (#46) * (fix): Bill should not return voided line items * Remove voided line items and payments from service level * filter voided line items in get bill by receipt number * Add filtering logic to the existing null remover function * Add transaction annotation to implementation * Review feedback * rename include voided items param to include all * Review feedback * Review feedback * O3-5187: Add server-side pagination (#53) * (feat) O3-5057: Add server-side pagination * Use alreadypaged with totalcount * Correct rebase * O3-5178: Exclude voided payments from bill status calculation (#48) * (feat): Allow filtering for multiple statuses (#58) * O3-5215: Enable Custom Representation for BillLineItemResource and PaymentResource (#62) * O3-5211: Update Billing Status When a Bill Line Item is Deleted (#63) * (fix) Generated bill should not get corrupted (#59) * fix generating Bill Reciept * fetch bill Reciept by uuid * remove redundant logs and raduce reciept height * Add patient name filtering support in Bill search (#68) * O3-5200: Bill should allow modifications only in pending state (#64) * O3-5156: Fix Biling itemList update functionality (#55) * Fix for concurrent issue * Addition of the test case * TestAddition plus usage of Set * O3-5156: Test Completion * O3-5156: Test Completion * Test changes * Test changes * Final changes * Review comment changes * Review comment changes * Code review --------- Co-authored-by: Ian * Migrate to Platform 2.7.x and support Java 21 (#69) * O3-5067: Replace JSON-based billing exemptions with database-backed service implementation (#57) * O3-5246: Fixing the payments issue Post addition of Pending state check (#72) * Migrate the BillService to OpenMRSService (#77) --------- Co-authored-by: Ian <52504170+ibacher@users.noreply.github.com> Co-authored-by: Wikum Weerakutti Co-authored-by: Ian Co-authored-by: Nethmi Rodrigo Co-authored-by: Raj Prakash Co-authored-by: Mutesasira Moses Co-authored-by: JG <85500670+jayg2002@users.noreply.github.com> --- .github/workflows/maven.yml | 2 +- api/pom.xml | 14 +- .../advice/GenerateBillFromOrderAdvice.java | 105 ++-- .../OrderCreationMethodBeforeAdvice.java | 7 +- .../billing/api/BillExemptionService.java | 20 + .../module/billing/api/BillService.java | 152 +++++ .../module/billing/api/IBillService.java | 119 ---- .../billing/api/IBillableItemsService.java | 4 +- .../billing/api/ICashierItemPriceService.java | 4 +- .../module/billing/api/ItemPriceService.java | 6 +- .../module/billing/api/base/PagingInfo.java | 42 +- .../api/base/entity/IObjectDataService.java | 2 +- .../impl/BaseEntityDataServiceImpl.java | 4 +- .../impl/BaseMetadataDataServiceImpl.java | 4 +- .../impl/BaseObjectDataServiceImpl.java | 4 +- .../entity/model/BaseCustomizableData.java | 2 +- .../module/billing/api/db/BillDAO.java | 110 ++++ .../billing/api/db/BillExemptionDAO.java | 24 + .../db/hibernate/BillExemptionDAOImpl.java | 94 +++ .../db/hibernate/HibernateBillDAOImpl.java | 191 ++++++ .../api/evaluator/ExemptionEvaluator.java | 11 + .../api/evaluator/ExemptionRuleEngine.java | 35 ++ .../billing/api/evaluator/ScriptType.java | 16 + .../evaluator/impl/JSExemptionEvaluator.java | 67 ++ .../api/impl/BillExemptionServiceImpl.java | 48 ++ .../api/impl/BillLineItemServiceImpl.java | 48 +- .../billing/api/impl/BillServiceImpl.java | 585 +++--------------- .../api/impl/BillableItemsServiceImpl.java | 10 +- .../impl/ICashierItemPriceServiceImpl.java | 10 +- .../api/impl/ItemPriceServiceImpl.java | 14 +- .../module/billing/api/model/Bill.java | 180 +----- .../billing/api/model/BillExemption.java | 82 +++ .../billing/api/model/BillExemptionRule.java | 78 +++ .../billing/api/model/BillLineItem.java | 7 +- .../billing/api/model/BillableService.java | 4 +- .../billing/api/model/CashierItemPrice.java | 4 +- .../billing/api/model/ExemptionType.java | 7 + .../module/billing/api/search/BillSearch.java | 60 +- .../api/search/BillableServiceSearch.java | 4 +- .../billing/api/util/PrivilegeConstants.java | 2 + .../exemptions/BillingExemptionChecker.java | 26 - .../billing/exemptions/BillingExemptions.java | 50 -- .../exemptions/BillingExemptionsConfig.java | 28 - .../exemptions/DefaultBillingExemptions.java | 32 - .../SampleBillingExemptionBuilder.java | 126 ---- .../exemptions/SampleBillingExemptions.json | 50 -- .../module/billing/util/ReceiptGenerator.java | 285 +++++++++ .../openmrs/module/billing/util/Utils.java | 4 - .../billing/validator/BillValidator.java | 44 ++ api/src/main/resources/Bill.hbm.xml | 20 +- .../resources/moduleApplicationContext.xml | 81 ++- .../module/billing/IBillServiceTest.java | 464 -------------- .../module/billing/ICashPointServiceTest.java | 4 +- .../module/billing/ITimesheetServiceTest.java | 4 +- .../SequentialReceiptNumberGeneratorTest.java | 64 +- .../openmrs/module/billing/TestConstants.java | 2 + .../hibernate/BillExemptionDAOImplTest.java | 296 +++++++++ .../evaluator/ExemptionRuleEngineTest.java | 289 +++++++++ .../impl/JSExemptionEvaluatorTest.java | 119 ++++ .../impl/BillExemptionServiceImplTest.java | 137 ++++ .../module/billing/api/model/BillTest.java | 195 ++++++ .../base/entity/IObjectDataServiceTest.java | 22 +- .../billing/db/HibernateBillDAOImplTest.java | 243 ++++++++ .../billing/impl/BillServiceImplTest.java | 508 +++++++++++---- .../impl/CashPointServiceImplTest.java | 190 ++++++ .../impl/CashierOptionsServiceGpImplTest.java | 388 ++++++------ .../billing/validator/BillValidatorTest.java | 77 +++ .../billing/api/include/BillExemptionTest.xml | 107 ++++ .../module/billing/api/include/BillTest.xml | 57 +- .../billing/api/include/CoreTest-2.0.xml | 3 + .../api/include/StockOperationType.xml | 41 ++ fhir/pom.xml | 18 +- .../impl/FhirInvoiceServiceImplTest.java | 1 - lombok.config | 3 + omod/pom.xml | 2 +- .../base/resource/BaseRestDataResource.java | 9 +- .../resource/BaseRestMetadataResource.java | 2 +- .../base/resource/BaseRestObjectResource.java | 2 +- ...tractSequentialReceiptNumberGenerator.java | 2 +- .../controller/BillAddEditController.java | 9 +- .../controller/CashierController.java | 2 +- .../PatientBillHistoryController.java | 10 +- .../legacyweb/filter/CashierLogoutFilter.java | 2 +- .../controller/CashierRestController.java | 2 +- .../rest/controller/ReceiptController.java | 17 +- .../rest/resource/BillExemptionResource.java | 142 +++++ .../resource/BillExemptionRuleResource.java | 161 +++++ .../rest/resource/BillLineItemResource.java | 3 +- .../web/rest/resource/BillResource.java | 117 +++- .../resource/BillableServiceResource.java | 11 +- .../resource/CashierItemPriceResource.java | 8 +- .../web/rest/resource/PaymentResource.java | 24 +- omod/src/main/resources/liquibase.xml | 150 +++++ pom.xml | 40 +- 94 files changed, 4658 insertions(+), 2217 deletions(-) create mode 100644 api/src/main/java/org/openmrs/module/billing/api/BillExemptionService.java create mode 100644 api/src/main/java/org/openmrs/module/billing/api/BillService.java delete mode 100644 api/src/main/java/org/openmrs/module/billing/api/IBillService.java create mode 100644 api/src/main/java/org/openmrs/module/billing/api/db/BillDAO.java create mode 100644 api/src/main/java/org/openmrs/module/billing/api/db/BillExemptionDAO.java create mode 100644 api/src/main/java/org/openmrs/module/billing/api/db/hibernate/BillExemptionDAOImpl.java create mode 100644 api/src/main/java/org/openmrs/module/billing/api/db/hibernate/HibernateBillDAOImpl.java create mode 100644 api/src/main/java/org/openmrs/module/billing/api/evaluator/ExemptionEvaluator.java create mode 100644 api/src/main/java/org/openmrs/module/billing/api/evaluator/ExemptionRuleEngine.java create mode 100644 api/src/main/java/org/openmrs/module/billing/api/evaluator/ScriptType.java create mode 100644 api/src/main/java/org/openmrs/module/billing/api/evaluator/impl/JSExemptionEvaluator.java create mode 100644 api/src/main/java/org/openmrs/module/billing/api/impl/BillExemptionServiceImpl.java create mode 100644 api/src/main/java/org/openmrs/module/billing/api/model/BillExemption.java create mode 100644 api/src/main/java/org/openmrs/module/billing/api/model/BillExemptionRule.java create mode 100644 api/src/main/java/org/openmrs/module/billing/api/model/ExemptionType.java delete mode 100644 api/src/main/java/org/openmrs/module/billing/exemptions/BillingExemptionChecker.java delete mode 100644 api/src/main/java/org/openmrs/module/billing/exemptions/BillingExemptions.java delete mode 100644 api/src/main/java/org/openmrs/module/billing/exemptions/BillingExemptionsConfig.java delete mode 100644 api/src/main/java/org/openmrs/module/billing/exemptions/DefaultBillingExemptions.java delete mode 100644 api/src/main/java/org/openmrs/module/billing/exemptions/SampleBillingExemptionBuilder.java delete mode 100644 api/src/main/java/org/openmrs/module/billing/exemptions/SampleBillingExemptions.json create mode 100644 api/src/main/java/org/openmrs/module/billing/util/ReceiptGenerator.java create mode 100644 api/src/main/java/org/openmrs/module/billing/validator/BillValidator.java delete mode 100644 api/src/test/java/org/openmrs/module/billing/IBillServiceTest.java create mode 100644 api/src/test/java/org/openmrs/module/billing/api/db/hibernate/BillExemptionDAOImplTest.java create mode 100644 api/src/test/java/org/openmrs/module/billing/api/evaluator/ExemptionRuleEngineTest.java create mode 100644 api/src/test/java/org/openmrs/module/billing/api/evaluator/impl/JSExemptionEvaluatorTest.java create mode 100644 api/src/test/java/org/openmrs/module/billing/api/impl/BillExemptionServiceImplTest.java create mode 100644 api/src/test/java/org/openmrs/module/billing/api/model/BillTest.java create mode 100644 api/src/test/java/org/openmrs/module/billing/db/HibernateBillDAOImplTest.java create mode 100644 api/src/test/java/org/openmrs/module/billing/impl/CashPointServiceImplTest.java create mode 100644 api/src/test/java/org/openmrs/module/billing/validator/BillValidatorTest.java create mode 100644 api/src/test/resources/org/openmrs/module/billing/api/include/BillExemptionTest.xml create mode 100644 api/src/test/resources/org/openmrs/module/billing/api/include/StockOperationType.xml create mode 100644 lombok.config create mode 100644 omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillExemptionResource.java create mode 100644 omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillExemptionRuleResource.java diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 74ee1259..94817daa 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: platform: [ ubuntu-latest ] - java-version: [ 8 ] + java-version: [ 8, 11, 17, 21 ] runs-on: ${{ matrix.platform }} env: diff --git a/api/pom.xml b/api/pom.xml index 161e308e..80f2aed5 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -4,7 +4,7 @@ org.openmrs.module billing - 1.3.3-SNAPSHOT + 2.0.0-SNAPSHOT billing-api @@ -87,7 +87,17 @@ com.itextpdf font-asian - + + + org.mockito + mockito-inline + test + + + org.projectlombok + lombok + + diff --git a/api/src/main/java/org/openmrs/module/billing/advice/GenerateBillFromOrderAdvice.java b/api/src/main/java/org/openmrs/module/billing/advice/GenerateBillFromOrderAdvice.java index 597771cb..5f8f34fb 100644 --- a/api/src/main/java/org/openmrs/module/billing/advice/GenerateBillFromOrderAdvice.java +++ b/api/src/main/java/org/openmrs/module/billing/advice/GenerateBillFromOrderAdvice.java @@ -12,20 +12,22 @@ import org.openmrs.api.OrderService; import org.openmrs.api.ProgramWorkflowService; import org.openmrs.api.context.Context; -import org.openmrs.module.billing.api.IBillService; +import org.openmrs.module.billing.api.BillExemptionService; +import org.openmrs.module.billing.api.BillService; import org.openmrs.module.billing.api.IBillableItemsService; import org.openmrs.module.billing.api.ICashPointService; import org.openmrs.module.billing.api.ItemPriceService; +import org.openmrs.module.billing.api.evaluator.ExemptionRuleEngine; import org.openmrs.module.billing.api.model.Bill; +import org.openmrs.module.billing.api.model.BillExemption; import org.openmrs.module.billing.api.model.BillLineItem; import org.openmrs.module.billing.api.model.BillStatus; import org.openmrs.module.billing.api.model.BillableService; import org.openmrs.module.billing.api.model.BillableServiceStatus; import org.openmrs.module.billing.api.model.CashPoint; import org.openmrs.module.billing.api.model.CashierItemPrice; +import org.openmrs.module.billing.api.model.ExemptionType; import org.openmrs.module.billing.api.search.BillableServiceSearch; -import org.openmrs.module.billing.exemptions.BillingExemptions; -import org.openmrs.module.billing.util.Utils; import org.openmrs.module.stockmanagement.api.StockManagementService; import org.openmrs.module.stockmanagement.api.model.StockItem; import org.springframework.aop.AfterReturningAdvice; @@ -34,10 +36,9 @@ import java.math.BigDecimal; import java.util.ArrayList; import java.util.Date; -import java.util.HashSet; +import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.stream.Collectors; public class GenerateBillFromOrderAdvice implements AfterReturningAdvice { @@ -48,7 +49,7 @@ public class GenerateBillFromOrderAdvice implements AfterReturningAdvice { OrderService orderService = Context.getOrderService(); - IBillService billService = Context.getService(IBillService.class); + BillService billService = Context.getService(BillService.class); StockManagementService stockService = Context.getService(StockManagementService.class); @@ -56,6 +57,10 @@ public class GenerateBillFromOrderAdvice implements AfterReturningAdvice { ICashPointService cashPointService = Context.getService(ICashPointService.class); + ExemptionRuleEngine exemptionRuleEngine = Context.getRegisteredComponent("ruleEngine", ExemptionRuleEngine.class); + + BillExemptionService billExemptionService = Context.getService(BillExemptionService.class); + /** * This is called immediately an order is saved */ @@ -90,7 +95,7 @@ public void afterReturning(Object returnValue, Method method, Object[] args, Obj if (!stockItems.isEmpty()) { // check from the list for all exemptions - boolean isExempted = checkIfOrderIsExempted(workflowService, order, BillingExemptions.COMMODITIES); + boolean isExempted = checkIfOrderIsExempted(workflowService, order, ExemptionType.COMMODITY); BillStatus lineItemStatus = isExempted ? BillStatus.EXEMPTED : BillStatus.PENDING; addBillItemToBill(order, patient, cashierUUID, stockItems.get(0), null, (int) drugQuantity, order.getDateActivated(), lineItemStatus); @@ -104,7 +109,7 @@ public void afterReturning(Object returnValue, Method method, Object[] args, Obj IBillableItemsService service = Context.getService(IBillableItemsService.class); List searchResult = service.findServices(new BillableServiceSearch(searchTemplate)); if (!searchResult.isEmpty()) { - boolean isExempted = checkIfOrderIsExempted(workflowService, order, BillingExemptions.SERVICES); + boolean isExempted = checkIfOrderIsExempted(workflowService, order, ExemptionType.SERVICE); BillStatus lineItemStatus = isExempted ? BillStatus.EXEMPTED : BillStatus.PENDING; addBillItemToBill(order, patient, cashierUUID, null, searchResult.get(0), 1, order.getDateActivated(), lineItemStatus); @@ -117,58 +122,56 @@ public void afterReturning(Object returnValue, Method method, Object[] args, Obj } } - /** - * Checks if an order concept is in the exemptions list - * - * @param workflowService - * @param order - * @param config - * @return - */ private boolean checkIfOrderIsExempted(ProgramWorkflowService workflowService, Order order, - Map> config) { - if (config == null || order == null || config.size() == 0) { + ExemptionType exemptionType) { + if (order == null || order.getConcept() == null) { return false; } - if (config.get("all") != null && config.get("all").contains(order.getConcept().getConceptId())) { - return true; + List exemptions = billExemptionService.getExemptionsByConcept(order.getConcept(), exemptionType, + false); + + if (exemptions == null || exemptions.isEmpty()) { + return false; } - // check in programs list - List programExemptions = config.keySet().stream().filter(key -> key.startsWith("program:")) - .collect(Collectors.toList()); - if (programExemptions.size() > 0) { - List programs = workflowService.getPatientPrograms(order.getPatient(), null, null, null, - new Date(), null, false); - Set activeEnrollments = new HashSet<>(); - programs.forEach(patientProgram -> { - if (patientProgram.getActive()) { - activeEnrollments.add(patientProgram.getProgram().getName()); - } - }); - - for (String programEntry : programExemptions) { - if (programEntry.contains(":")) { // this is our convention to distinguish program exemption - String programName = programEntry.substring(programEntry.indexOf(":") + 1); - //check if patient is active in the program - if (activeEnrollments.contains(programName)) { - // check if order is exempted - if (config.get(programEntry).contains(order.getConcept().getConceptId())) { - return true; - } - - } - } + + Map variables = buildVariablesMap(order, workflowService); + + for (BillExemption exemption : exemptions) { + if (exemptionRuleEngine.isExemptionApplicable(exemption, variables)) { + return true; } } - // check age category - if (order.getPatient().getAge() < 5 && config.get("age<5") != null - && config.get("age<5").contains(order.getConcept().getConceptId())) { - return true; - } return false; } + private Map buildVariablesMap(Order order, ProgramWorkflowService workflowService) { + Map variables = new HashMap<>(); + + Patient patient = order.getPatient(); + variables.put("patient", patient); + // We cannot call getAge() method from Java Script + if (patient != null) { + variables.put("patientAge", patient.getAge()); + } + + Map orderData = new HashMap<>(); + orderData.put("uuid", order.getUuid()); + if (order.getConcept() != null) { + orderData.put("conceptId", order.getConcept().getConceptId()); + } + variables.put("order", orderData); + + List programs = workflowService.getPatientPrograms(patient, null, null, null, new Date(), null, + false); + List activePrograms = programs.stream().filter(PatientProgram::getActive) + .map(pp -> pp.getProgram().getName()).collect(Collectors.toList()); + + variables.put("activePrograms", activePrograms); + + return variables; + } + /** * Adds a bill item to the cashier module * @@ -218,7 +221,7 @@ public void addBillItemToBill(Order order, Patient patient, String cashierUUID, activeBill.setCashPoint(cashPoints.get(0)); activeBill.addLineItem(billLineItem); activeBill.setStatus(BillStatus.PENDING); - billService.save(activeBill); + billService.saveBill(activeBill); } else { LOG.error("User is not a provider"); } diff --git a/api/src/main/java/org/openmrs/module/billing/advice/OrderCreationMethodBeforeAdvice.java b/api/src/main/java/org/openmrs/module/billing/advice/OrderCreationMethodBeforeAdvice.java index 4030df12..c46f0a97 100644 --- a/api/src/main/java/org/openmrs/module/billing/advice/OrderCreationMethodBeforeAdvice.java +++ b/api/src/main/java/org/openmrs/module/billing/advice/OrderCreationMethodBeforeAdvice.java @@ -32,7 +32,7 @@ import org.openmrs.VisitAttribute; import org.openmrs.api.OrderService; import org.openmrs.api.context.Context; -import org.openmrs.module.billing.api.IBillService; +import org.openmrs.module.billing.api.BillService; import org.openmrs.module.billing.api.IBillableItemsService; import org.openmrs.module.billing.api.ICashPointService; import org.openmrs.module.billing.api.ItemPriceService; @@ -44,7 +44,6 @@ import org.openmrs.module.billing.api.model.CashPoint; import org.openmrs.module.billing.api.model.CashierItemPrice; import org.openmrs.module.billing.api.search.BillableServiceSearch; -import org.openmrs.module.billing.util.Utils; import org.openmrs.module.stockmanagement.api.StockManagementService; import org.openmrs.module.stockmanagement.api.model.StockItem; import org.springframework.aop.MethodBeforeAdvice; @@ -57,7 +56,7 @@ public class OrderCreationMethodBeforeAdvice implements MethodBeforeAdvice { OrderService orderService = Context.getOrderService(); - IBillService billService = Context.getService(IBillService.class); + BillService billService = Context.getService(BillService.class); StockManagementService stockService = Context.getService(StockManagementService.class); @@ -166,7 +165,7 @@ public void addBillItemToBill(Order order, Patient patient, String cashierUUID, activeBill.setCashPoint(cashPoints.get(0)); activeBill.addLineItem(billLineItem); activeBill.setStatus(BillStatus.PENDING); - billService.save(activeBill); + billService.saveBill(activeBill); } } diff --git a/api/src/main/java/org/openmrs/module/billing/api/BillExemptionService.java b/api/src/main/java/org/openmrs/module/billing/api/BillExemptionService.java new file mode 100644 index 00000000..f806c35c --- /dev/null +++ b/api/src/main/java/org/openmrs/module/billing/api/BillExemptionService.java @@ -0,0 +1,20 @@ +package org.openmrs.module.billing.api; + +import org.openmrs.Concept; +import org.openmrs.module.billing.api.model.BillExemption; +import org.openmrs.module.billing.api.model.ExemptionType; + +import java.util.List; + +public interface BillExemptionService { + + BillExemption save(BillExemption billExemption); + + BillExemption getBillingExemptionById(Integer id); + + BillExemption getBillingExemptionByUuid(String uuid); + + List getExemptionsByConcept(Concept concept, ExemptionType itemType, boolean includeRetired); + + List getExemptionsByItemType(ExemptionType itemType, boolean includeRetired); +} diff --git a/api/src/main/java/org/openmrs/module/billing/api/BillService.java b/api/src/main/java/org/openmrs/module/billing/api/BillService.java new file mode 100644 index 00000000..cdfe0a87 --- /dev/null +++ b/api/src/main/java/org/openmrs/module/billing/api/BillService.java @@ -0,0 +1,152 @@ +package org.openmrs.module.billing.api; + +import org.openmrs.annotation.Authorized; +import org.openmrs.api.OpenmrsService; +import org.openmrs.module.billing.api.base.PagingInfo; +import org.openmrs.module.billing.api.model.Bill; +import org.openmrs.module.billing.api.search.BillSearch; +import org.openmrs.module.billing.api.util.PrivilegeConstants; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * Service interface for managing billing operations + * + * @see Bill + * @see BillSearch + */ +public interface BillService extends OpenmrsService { + + /** + * Retrieves a bill by its database ID. + * + * @param id the database ID of the bill + * @return the bill with the specified ID, or null if not found + * @throws org.openmrs.api.APIAuthenticationException if the user lacks VIEW_BILLS privilege + */ + @Transactional(readOnly = true) + @Authorized(PrivilegeConstants.VIEW_BILLS) + Bill getBill(Integer id); + + /** + * Retrieves a bill by its UUID. + * + * @param uuid the UUID of the bill + * @return the bill with the specified UUID, or null if not found + * @throws org.openmrs.api.APIAuthenticationException if the user lacks VIEW_BILLS privilege + */ + @Transactional(readOnly = true) + @Authorized(PrivilegeConstants.VIEW_BILLS) + Bill getBillByUuid(String uuid); + + /** + * Retrieves a bill by its receipt number. + * + * @param receiptNumber the receipt number of the bill + * @return the bill with the specified receipt number, or null if not found + * @throws org.openmrs.api.APIAuthenticationException if the user lacks VIEW_BILLS privilege + */ + @Transactional(readOnly = true) + @Authorized(PrivilegeConstants.VIEW_BILLS) + Bill getBillByReceiptNumber(String receiptNumber); + + /** + * Retrieves all bills for a specific patient. + * + * @param patientUuid the UUID of the patient + * @param pagingInfo optional paging information (can be null for no paging) + * @return a list of bills for the patient, or an empty list if none found + * @throws org.openmrs.api.APIAuthenticationException if the user lacks VIEW_BILLS privilege + */ + @Transactional(readOnly = true) + @Authorized(PrivilegeConstants.VIEW_BILLS) + List getBillsByPatientUuid(String patientUuid, PagingInfo pagingInfo); + + /** + * Searches for bills using the specified search criteria. + *

+ * By default, voided bills are excluded from search results unless explicitly included via + * {@link BillSearch#setIncludeVoided(Boolean)}. + *

+ * + * @param billSearch the search criteria + * @param pagingInfo optional paging information (can be null for no paging) + * @return a list of bills matching the search criteria, or an empty list if none found + * @throws org.openmrs.api.APIAuthenticationException if the user lacks VIEW_BILLS privilege + * @see BillSearch + */ + @Transactional(readOnly = true) + @Authorized(PrivilegeConstants.VIEW_BILLS) + List getBills(BillSearch billSearch, PagingInfo pagingInfo); + + /** + * Generates and downloads a receipt for the specified bill. + * + * @param bill the bill for which to generate a receipt + * @return a byte array containing the receipt data (typically a PDF) + * @throws org.openmrs.api.APIAuthenticationException if the user lacks VIEW_BILLS privilege + */ + @Transactional(readOnly = true) + @Authorized(PrivilegeConstants.VIEW_BILLS) + byte[] downloadBillReceipt(Bill bill); + + /** + * Saves a bill to the database. + *

+ * If the bill is new (no ID), it will be created. If it already exists, it will be updated. The + * bill's status will be synchronized based on its payments. + *

+ * + * @param bill the bill to save + * @return the saved bill with updated metadata + * @throws org.openmrs.api.APIAuthenticationException if the user lacks MANAGE_BILLS privilege + * @throws IllegalArgumentException if the bill is null or invalid + */ + @Transactional + @Authorized(PrivilegeConstants.MANAGE_BILLS) + Bill saveBill(Bill bill); + + /** + * Permanently deletes a bill from the database. + *

+ * Warning: This operation cannot be undone. Consider using + * {@link #voidBill(Bill, String)} instead for soft deletion. + *

+ * + * @param bill the bill to permanently delete + * @throws org.openmrs.api.APIAuthenticationException if the user lacks PURGE_BILLS privilege + */ + @Authorized(PrivilegeConstants.PURGE_BILLS) + void purgeBill(Bill bill); + + /** + * Voids (soft deletes) a bill with a specified reason. + *

+ * Voided bills are hidden from normal queries but remain in the database for audit purposes. Voided + * bills can be restored using {@link #unvoidBill(Bill)}. + *

+ * + * @param bill the bill to void + * @param voidReason the reason for voiding the bill (required) + * @return the voided bill + * @throws org.openmrs.api.APIAuthenticationException if the user lacks DELETE_BILLS privilege + * @throws IllegalArgumentException if voidReason is null or empty + */ + @Authorized(PrivilegeConstants.DELETE_BILLS) + Bill voidBill(Bill bill, String voidReason); + + /** + * Restores a previously voided bill. + *

+ * This operation removes the void flag and makes the bill visible in normal queries again. + *

+ * + * @param bill the bill to restore + * @return the restored bill + * @throws org.openmrs.api.APIAuthenticationException if the user lacks DELETE_BILLS privilege + */ + @Authorized(PrivilegeConstants.DELETE_BILLS) + Bill unvoidBill(Bill bill); + +} diff --git a/api/src/main/java/org/openmrs/module/billing/api/IBillService.java b/api/src/main/java/org/openmrs/module/billing/api/IBillService.java deleted file mode 100644 index cdd705aa..00000000 --- a/api/src/main/java/org/openmrs/module/billing/api/IBillService.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * The contents of this file are subject to the OpenMRS Public License - * Version 1.1 (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * http://license.openmrs.org - * - * Software distributed under the License is distributed on an "AS IS" - * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the - * License for the specific language governing rights and limitations - * under the License. - * - * Copyright (C) OpenMRS, LLC. All Rights Reserved. - */ - -package org.openmrs.module.billing.api; - -import java.io.File; -import java.util.List; - -import org.openmrs.Patient; -import org.openmrs.annotation.Authorized; -import org.openmrs.module.billing.api.base.PagingInfo; -import org.openmrs.module.billing.api.base.entity.IEntityDataService; -import org.openmrs.module.billing.api.model.Bill; -import org.openmrs.module.billing.api.search.BillSearch; -import org.openmrs.module.billing.api.util.PrivilegeConstants; -import org.springframework.transaction.annotation.Transactional; - -/** - * Interface that represents classes which perform data operations for {@link Bill}s. - */ -@Transactional -public interface IBillService extends IEntityDataService { - - /** - * Gets the {@link Bill} with the specified receipt number or {@code null} if not found. - * - * @param receiptNumber The receipt number to search for. - * @return The {@link Bill} with the specified receipt number or {@code null}. - * @should throw IllegalArgumentException if the receipt number is null - * @should throw IllegalArgumentException if the receipt number is empty - * @should throw IllegalArgumentException if the receipt number is longer than 255 characters - * @should return the bill with the specified reciept number - * @should return null if the receipt number is not found - */ - @Transactional(readOnly = true) - @Authorized({ PrivilegeConstants.VIEW_BILLS }) - Bill getBillByReceiptNumber(String receiptNumber); - - /** - * Returns all {@link Bill}s for the specified patient with the specified paging. - * - * @param patient The {@link Patient}. - * @param paging The paging information. - * @return All of the bills for the specified patient. - * @should throw NullPointerException if patient is null - * @should return all bills for the specified patient - * @should return an empty list if the specified patient has no bills - */ - List getBillsByPatient(Patient patient, PagingInfo paging); - - /** - * Returns all {@link Bill}s for the specified patient with the specified paging. - * - * @param patientId The patient id. - * @param paging The paging information. - * @return All of the bills for the specified patient. - * @should throw IllegalArgumentException if the patientId is less than zero - * @should throw NullPointerException if patient is null - * @should return all bills for the specified patient - * @should return an empty list if the specified patient has no bills - */ - List getBillsByPatientId(int patientId, PagingInfo paging); - - /** - * Gets all bills using the specified {@link BillSearch} settings. - * - * @param billSearch The bill search settings. - * @return The bills found or an empty list if no bills were found. - */ - @Transactional(readOnly = true) - @Authorized({ PrivilegeConstants.VIEW_BILLS }) - List getBills(BillSearch billSearch); - - /** - * Gets all bills using the specified {@link BillSearch} settings. - * - * @param billSearch The bill search settings. - * @param pagingInfo The paging information. - * @return The bills found or an empty list if no bills were found. - * @should throw NullPointerException if bill search is null - * @should throw NullPointerException if bill search template object is null - * @should return an empty list if no bills are found via the search - * @should return bills filtered by cashier - * @should return bills filtered by cash point - * @should return bills filtered by patient - * @should return bills filtered by status - * @should return all bills if paging is null - * @should return paged bills if paging is specified - * @should not return retired bills from search unless specified - */ - @Transactional(readOnly = true) - @Authorized({ PrivilegeConstants.VIEW_BILLS }) - List getBills(BillSearch billSearch, PagingInfo pagingInfo); - - @Override - @Authorized(PrivilegeConstants.VIEW_BILLS) - Bill getByUuid(String uuid); - - /** - * Gets bill receipt using the specified {@link Bill} settings. - * - * @param bill The bill search settings. - * @return The receipt containing bill items. - */ - @Transactional(readOnly = true) - @Authorized({ PrivilegeConstants.VIEW_BILLS }) - byte[] downloadBillReceipt(Bill bill); -} diff --git a/api/src/main/java/org/openmrs/module/billing/api/IBillableItemsService.java b/api/src/main/java/org/openmrs/module/billing/api/IBillableItemsService.java index 920b6117..d827a1d9 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/IBillableItemsService.java +++ b/api/src/main/java/org/openmrs/module/billing/api/IBillableItemsService.java @@ -15,13 +15,13 @@ import java.util.List; -import org.openmrs.module.billing.api.base.entity.IEntityDataService; +import org.openmrs.module.billing.api.base.entity.IMetadataDataService; import org.openmrs.module.billing.api.model.BillableService; import org.openmrs.module.billing.api.search.BillableServiceSearch; import org.springframework.transaction.annotation.Transactional; @Transactional -public interface IBillableItemsService extends IEntityDataService { +public interface IBillableItemsService extends IMetadataDataService { List findServices(final BillableServiceSearch search); } diff --git a/api/src/main/java/org/openmrs/module/billing/api/ICashierItemPriceService.java b/api/src/main/java/org/openmrs/module/billing/api/ICashierItemPriceService.java index 385831f6..b530d6e3 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/ICashierItemPriceService.java +++ b/api/src/main/java/org/openmrs/module/billing/api/ICashierItemPriceService.java @@ -13,9 +13,9 @@ */ package org.openmrs.module.billing.api; -import org.openmrs.module.billing.api.base.entity.IEntityDataService; +import org.openmrs.module.billing.api.base.entity.IMetadataDataService; import org.openmrs.module.billing.api.model.CashierItemPrice; import org.springframework.transaction.annotation.Transactional; @Transactional -public interface ICashierItemPriceService extends IEntityDataService {} +public interface ICashierItemPriceService extends IMetadataDataService {} diff --git a/api/src/main/java/org/openmrs/module/billing/api/ItemPriceService.java b/api/src/main/java/org/openmrs/module/billing/api/ItemPriceService.java index e1208085..76d9511c 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/ItemPriceService.java +++ b/api/src/main/java/org/openmrs/module/billing/api/ItemPriceService.java @@ -15,16 +15,16 @@ import java.util.List; -import org.openmrs.module.billing.api.base.entity.IEntityDataService; +import org.openmrs.module.billing.api.base.entity.IMetadataDataService; import org.openmrs.module.billing.api.model.BillableService; import org.openmrs.module.billing.api.model.CashierItemPrice; import org.openmrs.module.stockmanagement.api.model.StockItem; import org.springframework.transaction.annotation.Transactional; @Transactional -public interface ItemPriceService extends IEntityDataService { +public interface ItemPriceService extends IMetadataDataService { - CashierItemPrice save(CashierItemPrice price); + CashierItemPrice saveBill(CashierItemPrice price); List getItemPrice(StockItem stockItem); diff --git a/api/src/main/java/org/openmrs/module/billing/api/base/PagingInfo.java b/api/src/main/java/org/openmrs/module/billing/api/base/PagingInfo.java index 5cde2fa3..414ceffc 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/base/PagingInfo.java +++ b/api/src/main/java/org/openmrs/module/billing/api/base/PagingInfo.java @@ -13,10 +13,19 @@ */ package org.openmrs.module.billing.api.base; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + /** * This class contains the paging information used by the entity services to paginate results. Both * page and pageSize are 1-based, defining either as 0 will cause paging to be ignored. */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder public class PagingInfo { private int page; @@ -27,9 +36,6 @@ public class PagingInfo { private boolean loadRecordCount; - public PagingInfo() { - } - /** * Creates a new {@link PagingInfo} instance. * @@ -39,45 +45,15 @@ public PagingInfo() { public PagingInfo(int page, int pageSize) { this.page = page; this.pageSize = pageSize; - this.loadRecordCount = true; } - public int getPage() { - return page; - } - - public void setPage(int page) { - this.page = page; - } - - public int getPageSize() { - return pageSize; - } - - public void setPageSize(int pageSize) { - this.pageSize = pageSize; - } - - public Long getTotalRecordCount() { - return totalRecordCount; - } - public void setTotalRecordCount(Long totalRecordCount) { this.totalRecordCount = totalRecordCount; - // If the total records is set to anything other than null, than don't reload the count this.loadRecordCount = totalRecordCount == null; } - public boolean shouldLoadRecordCount() { - return loadRecordCount; - } - - public void setLoadRecordCount(boolean loadRecordCount) { - this.loadRecordCount = loadRecordCount; - } - public Boolean hasMoreResults() { return ((long) page * pageSize) < totalRecordCount; } diff --git a/api/src/main/java/org/openmrs/module/billing/api/base/entity/IObjectDataService.java b/api/src/main/java/org/openmrs/module/billing/api/base/entity/IObjectDataService.java index 9a6e255c..b994f2ad 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/base/entity/IObjectDataService.java +++ b/api/src/main/java/org/openmrs/module/billing/api/base/entity/IObjectDataService.java @@ -50,7 +50,7 @@ public interface IObjectDataService extends OpenmrsServ * @should update the object successfully * @should create the object successfully */ - E save(E object); + E saveBill(E object); /** * Saves an object to the database along with the specified related {@link OpenmrsObject}'s within a diff --git a/api/src/main/java/org/openmrs/module/billing/api/base/entity/impl/BaseEntityDataServiceImpl.java b/api/src/main/java/org/openmrs/module/billing/api/base/entity/impl/BaseEntityDataServiceImpl.java index 4e06f094..d84e8fd8 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/base/entity/impl/BaseEntityDataServiceImpl.java +++ b/api/src/main/java/org/openmrs/module/billing/api/base/entity/impl/BaseEntityDataServiceImpl.java @@ -68,7 +68,7 @@ public void apply(OpenmrsData data) { if (!updatedObjects.isEmpty()) { return saveAll(entity, updatedObjects); } else { - return save(entity); + return saveBill(entity); } } @@ -104,7 +104,7 @@ public void apply(OpenmrsData data) { if (!updatedObjects.isEmpty()) { return saveAll(entity, updatedObjects); } else { - return save(entity); + return saveBill(entity); } } diff --git a/api/src/main/java/org/openmrs/module/billing/api/base/entity/impl/BaseMetadataDataServiceImpl.java b/api/src/main/java/org/openmrs/module/billing/api/base/entity/impl/BaseMetadataDataServiceImpl.java index 814a93bd..f798fd64 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/base/entity/impl/BaseMetadataDataServiceImpl.java +++ b/api/src/main/java/org/openmrs/module/billing/api/base/entity/impl/BaseMetadataDataServiceImpl.java @@ -78,7 +78,7 @@ public void apply(OpenmrsMetadata metadata) { if (!updatedObjects.isEmpty()) { return saveAll(entity, updatedObjects); } else { - return save(entity); + return saveBill(entity); } } @@ -122,7 +122,7 @@ public void apply(OpenmrsMetadata metadata) { if (!updatedObjects.isEmpty()) { return saveAll(entity, updatedObjects); } else { - return save(entity); + return saveBill(entity); } } diff --git a/api/src/main/java/org/openmrs/module/billing/api/base/entity/impl/BaseObjectDataServiceImpl.java b/api/src/main/java/org/openmrs/module/billing/api/base/entity/impl/BaseObjectDataServiceImpl.java index c0850bcf..e0362bf1 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/base/entity/impl/BaseObjectDataServiceImpl.java +++ b/api/src/main/java/org/openmrs/module/billing/api/base/entity/impl/BaseObjectDataServiceImpl.java @@ -102,7 +102,7 @@ public void setRepository(BaseHibernateRepository repository) { @Override @Transactional - public E save(E object) { + public E saveBill(E object) { P privileges = getPrivileges(); if (privileges != null && !StringUtils.isEmpty(privileges.getSavePrivilege())) { PrivilegeUtil.requirePrivileges(Context.getAuthenticatedUser(), privileges.getSavePrivilege()); @@ -320,7 +320,7 @@ protected void loadPagingTotal(PagingInfo pagingInfo, Criteria criteria) { criteria = repository.createCriteria(getEntityClass()); } - if (pagingInfo.shouldLoadRecordCount()) { + if (pagingInfo.getLoadRecordCount()) { // Copy the current projection and transformer which requires getting access to the underlying criteria // implementation Projection projection = null; diff --git a/api/src/main/java/org/openmrs/module/billing/api/base/entity/model/BaseCustomizableData.java b/api/src/main/java/org/openmrs/module/billing/api/base/entity/model/BaseCustomizableData.java index 7a059225..80cd711f 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/base/entity/model/BaseCustomizableData.java +++ b/api/src/main/java/org/openmrs/module/billing/api/base/entity/model/BaseCustomizableData.java @@ -30,7 +30,7 @@ public abstract class BaseCustomizableData> // @formatter:on public static final long serialVersionUID = 0L; - private Set attributes; + private Set attributes = new HashSet<>(); protected void onAddAttribute(TAttribute attribute) { // Just here to allow subclass to add custom logic diff --git a/api/src/main/java/org/openmrs/module/billing/api/db/BillDAO.java b/api/src/main/java/org/openmrs/module/billing/api/db/BillDAO.java new file mode 100644 index 00000000..8bca90ff --- /dev/null +++ b/api/src/main/java/org/openmrs/module/billing/api/db/BillDAO.java @@ -0,0 +1,110 @@ +package org.openmrs.module.billing.api.db; + +import org.openmrs.module.billing.api.base.PagingInfo; +import org.openmrs.module.billing.api.model.Bill; +import org.openmrs.module.billing.api.search.BillSearch; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Nonnull; +import java.util.List; + +/** + * Data Access Object (DAO) interface for {@link Bill} persistence operations. + * + * @see Bill + * @see BillSearch + */ +public interface BillDAO { + + /** + * Retrieves a bill by its database ID. + * + * @param id the database ID of the bill (must not be null) + * @return the bill with the specified ID, or null if not found + */ + @Transactional(readOnly = true) + Bill getBill(@Nonnull Integer id); + + /** + * Retrieves a bill by its UUID. + *

+ * Note: This method may return voided bills. Consider filtering voided records at the service layer + * if needed. + *

+ * + * @param uuid the UUID of the bill (must not be null) + * @return the bill with the specified UUID, or null if not found + */ + @Transactional(readOnly = true) + Bill getBillByUuid(@Nonnull String uuid); + + /** + * Persists a bill to the database. + *

+ * If the bill has no ID, it will be created as a new record. If it has an ID, the existing record + * will be updated. + *

+ * + * @param bill the bill to save (must not be null) + * @return the saved bill with updated metadata (timestamps, IDs, etc.) + */ + @Transactional + Bill saveBill(@Nonnull Bill bill); + + /** + * Retrieves a bill by its receipt number. + *

+ * Note: This method may return voided bills. Consider filtering voided records at the service layer + * if needed. + *

+ * + * @param receiptNumber the receipt number of the bill (must not be null) + * @return the bill with the specified receipt number, or null if not found + */ + @Transactional(readOnly = true) + Bill getBillByReceiptNumber(@Nonnull String receiptNumber); + + /** + * Retrieves all bills for a specific patient. + *

+ * Note: This method may return voided bills. Consider filtering voided records at the service layer + * if needed. + *

+ * + * @param patientUuid the UUID of the patient (must not be null) + * @param pagingInfo optional paging information (can be null for no paging) + * @return a list of bills for the patient, or an empty list if none found + */ + @Transactional(readOnly = true) + List getBillsByPatientUuid(@Nonnull String patientUuid, PagingInfo pagingInfo); + + /** + * Searches for bills using the specified search criteria. + *

+ * By default, voided bills are excluded from results unless + * {@link BillSearch#setIncludeVoided(Boolean)} is set to true. The search criteria support + * filtering by patient, cashier, cash point, and status. + *

+ * + * @param billSearch the search criteria (must not be null) + * @param pagingInfo optional paging information (can be null for no paging). When provided with + * {@code loadRecordCount=true}, the total count will be populated in the pagingInfo + * @return a list of bills matching the search criteria, or an empty list if none found + * @see BillSearch + */ + @Transactional(readOnly = true) + List getBills(@Nonnull BillSearch billSearch, PagingInfo pagingInfo); + + /** + * Permanently deletes a bill from the database. + *

+ * Warning: This operation cannot be undone. All associated data (line items, + * payments, etc.) will also be removed due to cascade delete rules. + *

+ * + * @param bill the bill to permanently delete (must not be null) + */ + @Transactional + void purgeBill(@Nonnull Bill bill); + +} diff --git a/api/src/main/java/org/openmrs/module/billing/api/db/BillExemptionDAO.java b/api/src/main/java/org/openmrs/module/billing/api/db/BillExemptionDAO.java new file mode 100644 index 00000000..c9581215 --- /dev/null +++ b/api/src/main/java/org/openmrs/module/billing/api/db/BillExemptionDAO.java @@ -0,0 +1,24 @@ +package org.openmrs.module.billing.api.db; + +import org.openmrs.Concept; +import org.openmrs.module.billing.api.model.BillExemption; +import org.openmrs.module.billing.api.model.ExemptionType; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +public interface BillExemptionDAO { + + BillExemption save(BillExemption billExemption); + + BillExemption getBillingExemptionById(Integer id); + + @Transactional(readOnly = true) + BillExemption getBillingExemptionByUuid(String uuid); + + @Transactional(readOnly = true) + List getExemptionsByConcept(Concept concept, ExemptionType itemType, boolean includeRetired); + + @Transactional(readOnly = true) + List getExemptionsByItemType(ExemptionType itemType, boolean includeRetired); +} diff --git a/api/src/main/java/org/openmrs/module/billing/api/db/hibernate/BillExemptionDAOImpl.java b/api/src/main/java/org/openmrs/module/billing/api/db/hibernate/BillExemptionDAOImpl.java new file mode 100644 index 00000000..15e98d0f --- /dev/null +++ b/api/src/main/java/org/openmrs/module/billing/api/db/hibernate/BillExemptionDAOImpl.java @@ -0,0 +1,94 @@ +package org.openmrs.module.billing.api.db.hibernate; + +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.openmrs.Concept; +import org.openmrs.module.billing.api.db.BillExemptionDAO; +import org.openmrs.module.billing.api.model.BillExemption; +import org.openmrs.module.billing.api.model.ExemptionType; + +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; +import java.util.ArrayList; +import java.util.List; + +public class BillExemptionDAOImpl implements BillExemptionDAO { + + private final SessionFactory sessionFactory; + + public BillExemptionDAOImpl(SessionFactory sessionFactory) { + this.sessionFactory = sessionFactory; + } + + @Override + public BillExemption save(BillExemption billExemption) { + sessionFactory.getCurrentSession().saveOrUpdate(billExemption); + return billExemption; + } + + @Override + public BillExemption getBillingExemptionById(Integer id) { + return sessionFactory.getCurrentSession().get(BillExemption.class, id); + } + + @Override + public BillExemption getBillingExemptionByUuid(String uuid) { + Session session = sessionFactory.getCurrentSession(); + CriteriaBuilder cb = session.getCriteriaBuilder(); + CriteriaQuery query = cb.createQuery(BillExemption.class); + Root root = query.from(BillExemption.class); + + query.select(root).where(cb.equal(root.get("uuid"), uuid)); + return session.createQuery(query).getSingleResult(); + } + + @Override + public List getExemptionsByConcept(Concept concept, ExemptionType itemType, boolean includeRetired) { + Session session = sessionFactory.getCurrentSession(); + CriteriaBuilder cb = session.getCriteriaBuilder(); + CriteriaQuery query = cb.createQuery(BillExemption.class); + Root root = query.from(BillExemption.class); + + List predicates = new ArrayList<>(); + + if (concept != null) { + predicates.add(cb.equal(root.get("concept"), concept)); + } + + if (itemType != null) { + predicates.add(cb.equal(root.get("exemptionType"), itemType)); + } + + if (!includeRetired) { + predicates.add(cb.isFalse(root.get("retired"))); + } + + query.where(predicates.toArray(new Predicate[0])); + + return session.createQuery(query).getResultList(); + } + + @Override + public List getExemptionsByItemType(ExemptionType itemType, boolean includeRetired) { + Session session = sessionFactory.getCurrentSession(); + CriteriaBuilder cb = session.getCriteriaBuilder(); + CriteriaQuery query = cb.createQuery(BillExemption.class); + Root root = query.from(BillExemption.class); + + List predicates = new ArrayList<>(); + if (itemType != null) { + predicates.add(cb.equal(root.get("exemptionType"), itemType)); + } + + if (!includeRetired) { + predicates.add(cb.isFalse(root.get("retired"))); + } + + query.where(predicates.toArray(new Predicate[0])); + + return session.createQuery(query).getResultList(); + } + +} diff --git a/api/src/main/java/org/openmrs/module/billing/api/db/hibernate/HibernateBillDAOImpl.java b/api/src/main/java/org/openmrs/module/billing/api/db/hibernate/HibernateBillDAOImpl.java new file mode 100644 index 00000000..303e6be7 --- /dev/null +++ b/api/src/main/java/org/openmrs/module/billing/api/db/hibernate/HibernateBillDAOImpl.java @@ -0,0 +1,191 @@ +package org.openmrs.module.billing.api.db.hibernate; + +import org.apache.commons.lang3.StringUtils; +import org.openmrs.Patient; +import org.openmrs.api.context.Context; +import org.openmrs.api.db.hibernate.HibernatePatientDAO; +import org.openmrs.module.billing.api.base.PagingInfo; +import org.openmrs.module.billing.api.db.BillDAO; +import org.openmrs.module.billing.api.model.Bill; +import org.openmrs.module.billing.api.search.BillSearch; + +import javax.annotation.Nonnull; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.TypedQuery; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; +import java.util.ArrayList; +import java.util.List; + +/** + * Hibernate implementation of {@link BillDAO}. + * + * @see BillDAO + * @see Bill + */ +public class HibernateBillDAOImpl implements BillDAO { + + @PersistenceContext + private EntityManager entityManager; + + /** + * {@inheritDoc} + */ + @Override + public Bill getBill(@Nonnull Integer id) { + return entityManager.find(Bill.class, id); + } + + /** + * {@inheritDoc} + */ + @Override + public Bill getBillByUuid(@Nonnull String uuid) { + TypedQuery query = entityManager.createQuery("select b from Bill b where b.uuid = :uuid", Bill.class); + query.setParameter("uuid", uuid); + return query.getResultStream().findFirst().orElse(null); + } + + /** + * {@inheritDoc} + */ + @Override + public Bill saveBill(@Nonnull Bill bill) { + if (bill.getId() == null) { + entityManager.persist(bill); + return bill; + } + return entityManager.merge(bill); + } + + /** + * {@inheritDoc} + */ + @Override + public Bill getBillByReceiptNumber(@Nonnull String receiptNumber) { + TypedQuery query = entityManager.createQuery("select b from Bill b where b.receiptNumber = :receiptNumber", + Bill.class); + query.setParameter("receiptNumber", receiptNumber); + return query.getResultStream().findFirst().orElse(null); + } + + /** + * {@inheritDoc} + */ + @Override + public List getBillsByPatientUuid(@Nonnull String patientUuid, PagingInfo pagingInfo) { + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + CriteriaQuery cq = cb.createQuery(Bill.class); + Root root = cq.from(Bill.class); + + Predicate predicate = cb.equal(root.get("patient").get("uuid"), patientUuid); + cq.where(predicate); + + TypedQuery query = entityManager.createQuery(cq); + + List predicates = new ArrayList<>(); + predicates.add(predicate); + applyPaging(query, pagingInfo, predicates); + + return query.getResultList(); + } + + /** + * {@inheritDoc} + */ + @Override + public List getBills(@Nonnull BillSearch billSearch, PagingInfo pagingInfo) { + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + CriteriaQuery cq = cb.createQuery(Bill.class); + Root root = cq.from(Bill.class); + + List predicates = buildBillSearchPredicate(cb, root, billSearch); + + if (!predicates.isEmpty()) { + cq.where(predicates.toArray(new Predicate[0])); + } + + TypedQuery query = entityManager.createQuery(cq); + + applyPaging(query, pagingInfo, predicates); + + return query.getResultList(); + } + + /** + * {@inheritDoc} + */ + @Override + public void purgeBill(@Nonnull Bill bill) { + entityManager.remove(bill); + } + + private List buildBillSearchPredicate(CriteriaBuilder cb, Root root, BillSearch billSearch) { + List predicates = new ArrayList<>(); + + if (billSearch.getPatientUuid() != null) { + predicates.add(cb.equal(root.get("patient").get("uuid"), billSearch.getPatientUuid())); + } + + if (billSearch.getPatientName() != null && !billSearch.getPatientName().trim().isEmpty()) { + List matchingPatients = Context.getRegisteredComponent("patientDAO", HibernatePatientDAO.class) + .getPatients(billSearch.getPatientName(), 0, null); + if (matchingPatients != null && !matchingPatients.isEmpty()) { + predicates.add(root.get("patient").in(matchingPatients)); + } else { + predicates.add(cb.disjunction()); + } + } + + if (StringUtils.isNotEmpty(billSearch.getCashierUuid())) { + predicates.add(cb.equal(root.get("cashier").get("uuid"), billSearch.getCashierUuid())); + } + + if (billSearch.getCashPointUuid() != null) { + predicates.add(cb.equal(root.get("cashPoint").get("uuid"), billSearch.getCashPointUuid())); + } + + if (billSearch.getStatuses() != null && !billSearch.getStatuses().isEmpty()) { + predicates.add(root.get("status").in(billSearch.getStatuses())); + } + + if (!Boolean.TRUE.equals(billSearch.getIncludeVoided())) { + predicates.add(cb.equal(root.get("voided"), false)); + } + + return predicates; + } + + /** + * Applies paging to a query and optionally loads total record count. + * + * @param query The typed query to apply paging to + * @param pagingInfo The paging information (null to skip paging) + * @param predicates The predicates used for filtering (needed for count query) + */ + private void applyPaging(TypedQuery query, PagingInfo pagingInfo, List predicates) { + if (pagingInfo != null && pagingInfo.getPage() > 0 && pagingInfo.getPageSize() > 0) { + int offset = (pagingInfo.getPage() - 1) * pagingInfo.getPageSize(); + query.setFirstResult(offset); + query.setMaxResults(pagingInfo.getPageSize()); + + if (pagingInfo.getLoadRecordCount()) { + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + CriteriaQuery countQuery = cb.createQuery(Long.class); + Root countRoot = countQuery.from(Bill.class); + countQuery.select(cb.count(countRoot)); + + if (predicates != null && !predicates.isEmpty()) { + countQuery.where(predicates.toArray(new Predicate[0])); + } + + Long totalCount = entityManager.createQuery(countQuery).getSingleResult(); + pagingInfo.setTotalRecordCount(totalCount); + } + } + } + +} diff --git a/api/src/main/java/org/openmrs/module/billing/api/evaluator/ExemptionEvaluator.java b/api/src/main/java/org/openmrs/module/billing/api/evaluator/ExemptionEvaluator.java new file mode 100644 index 00000000..96ff36fe --- /dev/null +++ b/api/src/main/java/org/openmrs/module/billing/api/evaluator/ExemptionEvaluator.java @@ -0,0 +1,11 @@ +package org.openmrs.module.billing.api.evaluator; + +import java.util.Map; + +public interface ExemptionEvaluator { + + ScriptType getSupportedType(); + + boolean evaluate(String script, Map variables); + +} diff --git a/api/src/main/java/org/openmrs/module/billing/api/evaluator/ExemptionRuleEngine.java b/api/src/main/java/org/openmrs/module/billing/api/evaluator/ExemptionRuleEngine.java new file mode 100644 index 00000000..cd900c8d --- /dev/null +++ b/api/src/main/java/org/openmrs/module/billing/api/evaluator/ExemptionRuleEngine.java @@ -0,0 +1,35 @@ +package org.openmrs.module.billing.api.evaluator; + +import org.openmrs.module.billing.api.model.BillExemption; +import org.openmrs.module.billing.api.model.BillExemptionRule; + +import java.util.EnumMap; +import java.util.List; +import java.util.Map; + +public class ExemptionRuleEngine { + + private final Map evaluatorsByType = new EnumMap<>(ScriptType.class); + + public ExemptionRuleEngine(List evaluators) { + for (ExemptionEvaluator evaluator : evaluators) { + evaluatorsByType.put(evaluator.getSupportedType(), evaluator); + } + } + + public boolean evaluateRule(BillExemptionRule rule, Map variables) { + ExemptionEvaluator evaluator = evaluatorsByType.get(rule.getScriptType()); + if (evaluator == null) { + throw new IllegalArgumentException("Unsupported script type: " + rule.getScriptType()); + } + return evaluator.evaluate(rule.getScript(), variables); + } + + public boolean isExemptionApplicable(BillExemption exemption, Map variables) { + if (exemption.getRules() == null || exemption.getRules().isEmpty()) { + return false; + } + + return exemption.getRules().stream().filter(r -> !r.getVoided()).anyMatch(r -> evaluateRule(r, variables)); + } +} diff --git a/api/src/main/java/org/openmrs/module/billing/api/evaluator/ScriptType.java b/api/src/main/java/org/openmrs/module/billing/api/evaluator/ScriptType.java new file mode 100644 index 00000000..df2bea92 --- /dev/null +++ b/api/src/main/java/org/openmrs/module/billing/api/evaluator/ScriptType.java @@ -0,0 +1,16 @@ +package org.openmrs.module.billing.api.evaluator; + +public enum ScriptType { + + JAVASCRIPT("js"); + + private final String engineName; + + ScriptType(String engineName) { + this.engineName = engineName; + } + + public String getEngineName() { + return engineName; + } +} diff --git a/api/src/main/java/org/openmrs/module/billing/api/evaluator/impl/JSExemptionEvaluator.java b/api/src/main/java/org/openmrs/module/billing/api/evaluator/impl/JSExemptionEvaluator.java new file mode 100644 index 00000000..5082211b --- /dev/null +++ b/api/src/main/java/org/openmrs/module/billing/api/evaluator/impl/JSExemptionEvaluator.java @@ -0,0 +1,67 @@ +package org.openmrs.module.billing.api.evaluator.impl; + +import org.apache.commons.lang.StringEscapeUtils; +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.Value; +import org.openmrs.module.billing.api.evaluator.ExemptionEvaluator; +import org.openmrs.module.billing.api.evaluator.ScriptType; + +import java.util.Collections; +import java.util.Map; + +public class JSExemptionEvaluator implements ExemptionEvaluator { + + @Override + public ScriptType getSupportedType() { + return ScriptType.JAVASCRIPT; + } + + @Override + public boolean evaluate(String script, Map variables) { + try (Context context = Context.newBuilder("js").allowAllAccess(false).allowHostClassLookup(className -> false) + .build()) { + Value bindings = context.getBindings("js"); + + Map safeVars = (variables != null ? variables : Collections.emptyMap()); + + Value varsObject = convertMapToJSObject(context, safeVars); + bindings.putMember("vars", varsObject); + + for (Map.Entry entry : safeVars.entrySet()) { + Object value = entry.getValue(); + if (value instanceof Map) { + value = convertMapToJSObject(context, (Map) value); + } + bindings.putMember(entry.getKey(), value); + } + + Value result = context.eval("js", script); + + if (result.isBoolean()) { + return result.asBoolean(); + } + if (result.isNull()) { + return false; + } + return Boolean.parseBoolean(result.toString()); + } + catch (Exception e) { + throw new RuntimeException("Error evaluating JS exemption script: " + script, e); + } + } + + private Value convertMapToJSObject(Context context, Map map) { + Value jsObject = context.eval("js", "({})"); + for (Map.Entry entry : map.entrySet()) { + String key = entry.getKey().toString(); + Object value = entry.getValue(); + + if (value instanceof Map) { + value = convertMapToJSObject(context, (Map) value); + } + + jsObject.putMember(key, value); + } + return jsObject; + } +} diff --git a/api/src/main/java/org/openmrs/module/billing/api/impl/BillExemptionServiceImpl.java b/api/src/main/java/org/openmrs/module/billing/api/impl/BillExemptionServiceImpl.java new file mode 100644 index 00000000..076f35b0 --- /dev/null +++ b/api/src/main/java/org/openmrs/module/billing/api/impl/BillExemptionServiceImpl.java @@ -0,0 +1,48 @@ +package org.openmrs.module.billing.api.impl; + +import org.openmrs.Concept; +import org.openmrs.module.billing.api.BillExemptionService; +import org.openmrs.module.billing.api.db.BillExemptionDAO; +import org.openmrs.module.billing.api.model.BillExemption; +import org.openmrs.module.billing.api.model.ExemptionType; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service("billing.billingExemptionService") +@Transactional +public class BillExemptionServiceImpl implements BillExemptionService { + + private final BillExemptionDAO billExemptionDAO; + + public BillExemptionServiceImpl(BillExemptionDAO billExemptionDAO) { + this.billExemptionDAO = billExemptionDAO; + } + + @Override + public BillExemption save(BillExemption billExemption) { + return billExemptionDAO.save(billExemption); + } + + @Override + public BillExemption getBillingExemptionById(Integer id) { + return billExemptionDAO.getBillingExemptionById(id); + } + + @Override + public BillExemption getBillingExemptionByUuid(String uuid) { + return billExemptionDAO.getBillingExemptionByUuid(uuid); + } + + @Override + public List getExemptionsByConcept(Concept concept, ExemptionType itemType, boolean includeRetired) { + return billExemptionDAO.getExemptionsByConcept(concept, itemType, includeRetired); + } + + @Override + public List getExemptionsByItemType(ExemptionType itemType, boolean includeRetired) { + return billExemptionDAO.getExemptionsByItemType(itemType, includeRetired); + } + +} diff --git a/api/src/main/java/org/openmrs/module/billing/api/impl/BillLineItemServiceImpl.java b/api/src/main/java/org/openmrs/module/billing/api/impl/BillLineItemServiceImpl.java index 75473c35..deb97f2c 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/impl/BillLineItemServiceImpl.java +++ b/api/src/main/java/org/openmrs/module/billing/api/impl/BillLineItemServiceImpl.java @@ -13,9 +13,12 @@ */ package org.openmrs.module.billing.api.impl; +import org.openmrs.api.context.Context; import org.openmrs.module.billing.api.BillLineItemService; +import org.openmrs.module.billing.api.BillService; import org.openmrs.module.billing.api.base.entity.impl.BaseEntityDataServiceImpl; import org.openmrs.module.billing.api.base.entity.security.IEntityAuthorizationPrivileges; +import org.openmrs.module.billing.api.model.Bill; import org.openmrs.module.billing.api.model.BillLineItem; import org.springframework.transaction.annotation.Transactional; @@ -29,7 +32,6 @@ protected IEntityAuthorizationPrivileges getPrivileges() { @Override protected void validate(BillLineItem object) { - } @Override @@ -51,4 +53,48 @@ public String getPurgePrivilege() { public String getGetPrivilege() { return null; } + + @Override + public BillLineItem voidEntity(BillLineItem entity, String reason) { + BillLineItem voidedLineItem = super.voidEntity(entity, reason); + + if (voidedLineItem != null && voidedLineItem.getBill() != null) { + Bill bill = voidedLineItem.getBill(); + bill.synchronizeBillStatus(); + } + + return voidedLineItem; + } + + @Override + public BillLineItem unvoidEntity(BillLineItem entity) { + BillLineItem unvoidedLineItem = super.unvoidEntity(entity); + + if (unvoidedLineItem != null && unvoidedLineItem.getBill() != null) { + Bill bill = unvoidedLineItem.getBill(); + bill.synchronizeBillStatus(); + } + + return unvoidedLineItem; + } + + @Override + public void purge(BillLineItem entity) { + Bill bill = null; + if (entity != null && entity.getBill() != null) { + bill = entity.getBill(); + // Validate before purging (purge doesn't call validate()) + } + + super.purge(entity); + + if (bill != null) { + // Remove the line item from the bill's collection + bill.removeLineItem(entity); + bill.synchronizeBillStatus(); + // Save the bill to persist the collection change + BillService billService = Context.getService(BillService.class); + billService.saveBill(bill); + } + } } diff --git a/api/src/main/java/org/openmrs/module/billing/api/impl/BillServiceImpl.java b/api/src/main/java/org/openmrs/module/billing/api/impl/BillServiceImpl.java index a6f2c64d..4882aa04 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/impl/BillServiceImpl.java +++ b/api/src/main/java/org/openmrs/module/billing/api/impl/BillServiceImpl.java @@ -13,563 +13,142 @@ */ package org.openmrs.module.billing.api.impl; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.math.BigDecimal; -import java.net.MalformedURLException; -import java.net.URL; -import java.security.AccessControlException; -import java.text.DecimalFormat; -import java.util.Date; -import java.util.List; - -import com.itextpdf.io.font.constants.StandardFonts; -import com.itextpdf.io.image.ImageDataFactory; -import com.itextpdf.kernel.font.PdfFont; -import com.itextpdf.kernel.font.PdfFontFactory; -import com.itextpdf.kernel.geom.PageSize; -import com.itextpdf.kernel.geom.Rectangle; -import com.itextpdf.kernel.pdf.PdfDocument; -import com.itextpdf.kernel.pdf.PdfWriter; -import com.itextpdf.layout.Document; -import com.itextpdf.layout.borders.Border; -import com.itextpdf.layout.element.Cell; -import com.itextpdf.layout.element.IElement; -import com.itextpdf.layout.element.Image; -import com.itextpdf.layout.element.Paragraph; -import com.itextpdf.layout.element.Table; -import com.itextpdf.layout.element.Text; -import com.itextpdf.layout.properties.TextAlignment; -import com.itextpdf.layout.properties.UnitValue; -import org.apache.commons.lang.StringUtils; -import org.apache.commons.lang.WordUtils; -import org.hibernate.Criteria; -import org.hibernate.criterion.Order; -import org.hibernate.criterion.Restrictions; -import org.joda.time.DateTime; -import org.openmrs.GlobalProperty; -import org.openmrs.Patient; -import org.openmrs.annotation.Authorized; -import org.openmrs.api.AdministrationService; -import org.openmrs.api.context.Context; -import org.openmrs.module.billing.api.IBillService; -import org.openmrs.module.billing.api.IReceiptNumberGenerator; -import org.openmrs.module.billing.api.ReceiptNumberGeneratorFactory; +import lombok.Setter; +import org.apache.commons.lang3.StringUtils; +import org.openmrs.api.impl.BaseOpenmrsService; +import org.openmrs.module.billing.api.BillService; import org.openmrs.module.billing.api.base.PagingInfo; -import org.openmrs.module.billing.api.base.entity.impl.BaseEntityDataServiceImpl; -import org.openmrs.module.billing.api.base.entity.security.IEntityAuthorizationPrivileges; -import org.openmrs.module.billing.api.base.f.Action1; +import org.openmrs.module.billing.api.db.BillDAO; import org.openmrs.module.billing.api.model.Bill; -import org.openmrs.module.billing.api.model.BillLineItem; -import org.openmrs.module.billing.api.model.BillStatus; -import org.openmrs.module.billing.api.model.Payment; import org.openmrs.module.billing.api.search.BillSearch; -import org.openmrs.module.billing.api.util.PrivilegeConstants; -import org.openmrs.module.billing.util.Utils; -import org.openmrs.util.OpenmrsUtil; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.openmrs.module.billing.util.ReceiptGenerator; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; +import java.util.Collections; +import java.util.List; + /** - * Data service implementation class for {@link Bill}s. + * Default implementation of {@link BillService}. + *

+ * This class delegates to {@link BillDAO} for persistence operations. For detailed documentation of + * each method, see the interface {@link BillService}. + *

+ * + * @see BillService + * @see BillDAO */ @Transactional -public class BillServiceImpl extends BaseEntityDataServiceImpl implements IEntityAuthorizationPrivileges, IBillService { - - private static final int MAX_LENGTH_RECEIPT_NUMBER = 255; +public class BillServiceImpl extends BaseOpenmrsService implements BillService { - private static final Logger LOG = LoggerFactory.getLogger(BillServiceImpl.class); - - private static final String GP_DEFAULT_LOCATION = "defaultLocation"; - - private static final String GP_FACILITY_ADDRESS_DETAILS = "billing.receipt.facilityAddress"; - - private static final String GP_BILL_LOGO_PATH = "billing.receipt.logoPath"; + @Setter(onMethod_ = { @Autowired }) + private BillDAO billDAO; + /** + * {@inheritDoc} + */ @Override - protected IEntityAuthorizationPrivileges getPrivileges() { - return this; + public Bill getBill(Integer id) { + if (id == null) { + return null; + } + return billDAO.getBill(id); } - DecimalFormat df = new DecimalFormat("0.00"); - + /** + * {@inheritDoc} + */ @Override - protected void validate(Bill bill) { + public Bill getBillByUuid(String uuid) { + if (uuid == null) { + return null; + } + return billDAO.getBillByUuid(uuid); } /** - * Saves the bill to the database, creating a new bill or updating an existing one. - * - * @param bill The bill to be saved. - * @return The saved bill. - * @should Generate a new receipt number if one has not been defined. - * @should Not generate a receipt number if one has already been defined. - * @should Throw APIException if receipt number cannot be generated. + * {@inheritDoc} */ @Override - @Authorized({ PrivilegeConstants.MANAGE_BILLS }) - @Transactional - public Bill save(Bill bill) { + public Bill saveBill(Bill bill) { if (bill == null) { throw new NullPointerException("The bill must be defined."); } - - // Check for refund. - // A refund is given when the total of the bill's line items is negative. - if (bill.getTotal().compareTo(BigDecimal.ZERO) < 0 && !Context.hasPrivilege(PrivilegeConstants.REFUND_MONEY)) { - throw new AccessControlException("Access denied to give a refund."); - } - - // Generate a receipt number if it hasn't been defined - IReceiptNumberGenerator generator = ReceiptNumberGeneratorFactory.getGenerator(); - if (generator == null) { - LOG.warn( - "No receipt number generator has been defined. Bills will not be given a receipt number until one is defined."); - } else { - if (StringUtils.isEmpty(bill.getReceiptNumber())) { - bill.setReceiptNumber(generator.generateNumber(bill)); - } - } - // Check if there is an existing pending bill for the patient - List bills = searchBill(bill.getPatient()); - if (!bills.isEmpty()) { - Bill billToUpdate = bills.get(0); - billToUpdate.setStatus(BillStatus.PENDING); - for (BillLineItem item : bill.getLineItems()) { - item.setBill(billToUpdate); - billToUpdate.getLineItems().add(item); - } - - // Calculate the total payments made on the bill - BigDecimal totalPaid = billToUpdate.getPayments().stream().map(Payment::getAmountTendered) - .reduce(BigDecimal.ZERO, BigDecimal::add); - - // Check if the bill is fully paid - if (totalPaid.compareTo(billToUpdate.getTotal()) >= 0) { - billToUpdate.setStatus(BillStatus.PAID); - } else { - billToUpdate.setStatus(BillStatus.PENDING); - } - - // Save the updated bill - return super.save(billToUpdate); - } - - // If no pending bill exists, just save the new bill as it is - return super.save(bill); + return billDAO.saveBill(bill); } + /** + * {@inheritDoc} + */ @Override - @Authorized({ PrivilegeConstants.VIEW_BILLS }) - @Transactional(readOnly = true) public Bill getBillByReceiptNumber(String receiptNumber) { - if (StringUtils.isEmpty(receiptNumber)) { - throw new IllegalArgumentException("The receipt number must be defined."); - } - if (receiptNumber.length() > MAX_LENGTH_RECEIPT_NUMBER) { - throw new IllegalArgumentException("The receipt number must be less than 256 characters."); + if (receiptNumber == null) { + return null; } - - Criteria criteria = getRepository().createCriteria(getEntityClass()); - criteria.add(Restrictions.eq("receiptNumber", receiptNumber)); - - Bill bill = getRepository().selectSingle(getEntityClass(), criteria); - removeNullLineItems(bill); - return bill; - } - - @Override - public List getBillsByPatient(Patient patient, PagingInfo paging) { - if (patient == null) { - throw new NullPointerException("The patient must be defined."); - } - - return getBillsByPatientId(patient.getId(), paging); + return billDAO.getBillByReceiptNumber(receiptNumber); } + /** + * {@inheritDoc} + */ @Override - public List getBillsByPatientId(int patientId, PagingInfo paging) { - if (patientId < 0) { - throw new IllegalArgumentException("The patient id must be a valid identifier."); + public List getBillsByPatientUuid(String patientUuid, PagingInfo pagingInfo) { + if (StringUtils.isEmpty(patientUuid)) { + return Collections.emptyList(); } - - Criteria criteria = getRepository().createCriteria(getEntityClass()); - criteria.add(Restrictions.eq("patient.id", patientId)); - criteria.addOrder(Order.desc("id")); - - List results = getRepository().select(getEntityClass(), createPagingCriteria(paging, criteria)); - removeNullLineItems(results); - - return results; - } - - @Override - public List getBills(final BillSearch billSearch) { - return getBills(billSearch, null); + return billDAO.getBillsByPatientUuid(patientUuid, pagingInfo); } + /** + * {@inheritDoc} + */ @Override - public List getBills(final BillSearch billSearch, PagingInfo pagingInfo) { + public List getBills(BillSearch billSearch, PagingInfo pagingInfo) { if (billSearch == null) { - throw new NullPointerException("The bill search must be defined."); - } else if (billSearch.getTemplate() == null) { - throw new NullPointerException("The bill search template must be defined."); + return Collections.emptyList(); } - - return executeCriteria(Bill.class, pagingInfo, new Action1() { - - @Override - public void apply(Criteria criteria) { - billSearch.updateCriteria(criteria); - } - }); - } - - /* - These methods are overridden to ensure that any null line items (created as part of a bug in 1.7.0) are removed - from the results before being returned to the caller. - */ - @Override - public List getAll(boolean includeVoided, PagingInfo pagingInfo) { - List results = super.getAll(includeVoided, pagingInfo); - removeNullLineItems(results); - return results; - } - - @Override - public Bill getById(int entityId) { - Bill bill = super.getById(entityId); - removeNullLineItems(bill); - return bill; - } - - @Override - public Bill getByUuid(String uuid) { - Bill bill = super.getByUuid(uuid); - removeNullLineItems(bill); - return bill; + return billDAO.getBills(billSearch, pagingInfo); } /** - * Generate a pdf receipt - * - * @param bill The bill search settings. - * @return + * {@inheritDoc} */ @Override public byte[] downloadBillReceipt(Bill bill) { - AdministrationService administrationService = Context.getAdministrationService(); - Patient patient = bill.getPatient(); - String fullName = patient.getPersonName().getFullName(); - String gender = patient.getGender() != null ? patient.getGender() : ""; - String dob = patient.getBirthdate() != null ? Utils.getSimpleDateFormat("dd-MMM-yyyy").format(patient.getBirthdate()) - : ""; - - /** - * https://kb.itextpdf.com/home/it7kb/faq/how-to-set-the-page-size-to-envelope-size-with-landscape-orientation - * page size: 3.5inch length, 1.1 inch height 1mm = 0.0394 inch length = 450mm = 17.7165 inch = - * 127.5588 points height = 300mm = 11.811 inch = 85.0392 points The measurement system in PDF - * doesn't use inches, but user units. By default, 1 user unit = 1 point, and 1 inch = 72 points. - * Thermal printer: 4 x 10 inches paper 4 inches = 4 x 72 = 288 5 inches = 10 x 72 = 720 - */ - int FONT_SIZE_12 = 12; - Rectangle thermalPrinterPageSize = new Rectangle(288, 14400); - - PdfFont timesRoman; - PdfFont courierBold; - PdfFont helvetica; - PdfFont helveticaBold; - try { - timesRoman = PdfFontFactory.createFont(StandardFonts.TIMES_ROMAN); - courierBold = PdfFontFactory.createFont(StandardFonts.COURIER_BOLD); - helvetica = PdfFontFactory.createFont(StandardFonts.HELVETICA); - helveticaBold = PdfFontFactory.createFont(StandardFonts.HELVETICA_BOLD); - - } - catch (IOException e) { - throw new RuntimeException(e); - } - - PdfFont headerSectionFont = helveticaBold; - PdfFont billItemSectionFont = helvetica; - PdfFont footerSectionFont = courierBold; - URL logoUrl = null; - - String logoPath = administrationService.getGlobalProperty(GP_BILL_LOGO_PATH, ""); - if (StringUtils.isNotBlank(logoPath)) { - File file = new File(logoPath.trim()); - if (!file.isAbsolute()) { - file = new File(OpenmrsUtil.getApplicationDataDirectory(), logoPath.trim()); - } - - if (file.exists()) { - try { - logoUrl = file.getAbsoluteFile().toURI().toURL(); - } - catch (MalformedURLException e) { - LOG.error("Error Loading file: {}", file.getAbsoluteFile(), e); - } - } - } - - if (logoUrl == null) { - logoUrl = BillServiceImpl.class.getClassLoader().getResource("img/openmrs-logo.png"); - } - - Image logoImage = null; - if (logoUrl != null) { - logoImage = new Image(ImageDataFactory.create(logoUrl)); - logoImage.scaleToFit(80, 80); - } - Paragraph divider = new Paragraph("------------------------------------------------------------------"); - Text billDateLabel = new Text(Utils.getSimpleDateFormat("dd-MMM-yyyy HH:mm:ss").format(bill.getDateCreated())); - - GlobalProperty gp = administrationService.getGlobalPropertyObject(GP_DEFAULT_LOCATION); - //GlobalProperty gpFacilityAddress = Context.getAdministrationService().getGlobalPropertyObject(GP_FACILITY_ADDRESS_DETAILS); - //Text facilityName = new Text(gp != null && gp.getValue() != null ? ((Location) gp.getValue()).getName() - // : bill.getCashPoint().getLocation().getName()); - - //Text facilityAddressDetails = new Text(gpFacilityAddress != null && gpFacilityAddress.getValue() != null ? gpFacilityAddress.getPropertyValue(): ""); - Paragraph logoSection = null; - if (logoImage != null) { - logoSection = new Paragraph(); - logoSection.setFontSize(14); - logoSection.add(logoImage).add("\n"); - //logoSection.add(facilityName).add("\n"); - logoSection.setTextAlignment(TextAlignment.CENTER); - logoSection.setFont(timesRoman).setBold(); - } - - //Paragraph addressSection = new Paragraph(); - //addressSection.add(facilityAddressDetails).setTextAlignment(TextAlignment.CENTER).setFont(helvetica).setFontSize(12); - - float[] headerColWidth = { 2f, 7f }; - Table receiptHeader = new Table(headerColWidth); - receiptHeader.setWidth(UnitValue.createPercentValue(100f)); - - receiptHeader.addCell(new Paragraph("Date:")).setFontSize(FONT_SIZE_12).setTextAlignment(TextAlignment.LEFT) - .setFont(headerSectionFont); - receiptHeader.addCell(new Paragraph(billDateLabel.getText())).setFontSize(FONT_SIZE_12) - .setTextAlignment(TextAlignment.LEFT).setFont(helvetica); - - receiptHeader.addCell(new Paragraph("Receipt No:")).setFontSize(FONT_SIZE_12).setTextAlignment(TextAlignment.LEFT) - .setFont(headerSectionFont); - receiptHeader.addCell(new Paragraph(bill.getReceiptNumber())).setFontSize(FONT_SIZE_12) - .setTextAlignment(TextAlignment.LEFT).setFont(helvetica); - - receiptHeader.addCell(new Paragraph("Patient:")).setFontSize(FONT_SIZE_12).setTextAlignment(TextAlignment.LEFT) - .setFont(headerSectionFont); - receiptHeader.addCell(new Paragraph(WordUtils.capitalizeFully(fullName))).setFontSize(FONT_SIZE_12) - .setTextAlignment(TextAlignment.LEFT).setFont(helvetica); - - receiptHeader.addCell(new Paragraph("Gender:")).setFontSize(FONT_SIZE_12).setTextAlignment(TextAlignment.LEFT) - .setFont(headerSectionFont); - receiptHeader.addCell(new Paragraph(WordUtils.capitalizeFully(gender))).setFontSize(FONT_SIZE_12) - .setTextAlignment(TextAlignment.LEFT).setFont(helvetica); - - receiptHeader.addCell(new Paragraph("Date of Birth:")).setFontSize(FONT_SIZE_12).setTextAlignment(TextAlignment.LEFT) - .setFont(headerSectionFont); - receiptHeader.addCell(new Paragraph(WordUtils.capitalizeFully(dob))).setFontSize(FONT_SIZE_12) - .setTextAlignment(TextAlignment.LEFT).setFont(helvetica); - - float[] columnWidths = { 1f, 5f, 2f, 2f }; - Table billLineItemstable = new Table(columnWidths); - billLineItemstable.setBorder(Border.NO_BORDER); - billLineItemstable.setWidth(UnitValue.createPercentValue(100f)); - - billLineItemstable.addCell(new Paragraph("Qty").setTextAlignment(TextAlignment.LEFT)).setFontSize(FONT_SIZE_12) - .setTextAlignment(TextAlignment.LEFT); - billLineItemstable.addCell(new Paragraph("Item").setTextAlignment(TextAlignment.LEFT)).setFontSize(FONT_SIZE_12) - .setTextAlignment(TextAlignment.LEFT); - billLineItemstable.addCell(new Paragraph("Price")).setFontSize(FONT_SIZE_12).setTextAlignment(TextAlignment.RIGHT); - billLineItemstable.addCell(new Paragraph("Total")).setFontSize(FONT_SIZE_12).setTextAlignment(TextAlignment.RIGHT); - - for (BillLineItem item : bill.getLineItems()) { - addBillLineItem(item, billLineItemstable, billItemSectionFont); - } - - float[] totalColWidth = { 1f, 5f, 2f, 2f }; - Table totalsSection = new Table(totalColWidth); - totalsSection.setWidth(UnitValue.createPercentValue(100f)); - - totalsSection.addCell(new Paragraph(" ")); - totalsSection.addCell(new Paragraph(" ")); - totalsSection.addCell(new Paragraph("Total")).setFontSize(10).setTextAlignment(TextAlignment.RIGHT) - .setFont(helvetica).setBold(); - totalsSection.addCell(new Paragraph(df.format(bill.getTotal()))).setFontSize(10) - .setTextAlignment(TextAlignment.RIGHT).setFont(helvetica).setBold(); - - setInnerCellBorder(receiptHeader, Border.NO_BORDER); - setInnerCellBorder(billLineItemstable, Border.NO_BORDER); - - float[] paymentColWidth = { 1f, 5f, 2f, 2f }; - Table paymentSection = new Table(paymentColWidth); - paymentSection.setWidth(UnitValue.createPercentValue(100f)); - paymentSection.addCell(new Paragraph(" ")); - paymentSection.addCell(new Paragraph(" ")); - paymentSection.addCell(new Paragraph("Payment").setTextAlignment(TextAlignment.RIGHT).setBold()); - paymentSection.addCell(new Paragraph("")); - // append payment rows - for (Payment payment : bill.getPayments()) { - paymentSection.addCell(new Paragraph(" ")); - paymentSection.addCell(new Paragraph(" ")); - paymentSection.addCell(new Paragraph(payment.getInstanceType().getName()).setTextAlignment(TextAlignment.RIGHT)) - .setFontSize(10).setFont(helvetica); - paymentSection - .addCell(new Paragraph(df.format(payment.getAmountTendered())).setTextAlignment(TextAlignment.RIGHT)) - .setFontSize(10).setFont(helvetica); - } - - float[] amountDueColWidth = { 1f, 5f, 2f, 2f }; - Table amountDueSection = new Table(amountDueColWidth); - amountDueSection.setWidth(UnitValue.createPercentValue(100f)); - - amountDueSection.addCell(new Paragraph(" ")); - amountDueSection.addCell(new Paragraph(" ")); - - amountDueSection.addCell(new Paragraph("Due Amount")).setFontSize(10).setTextAlignment(TextAlignment.RIGHT) - .setFont(helvetica).setBold(); - BigDecimal dueAmount = bill.getTotal().subtract(bill.getTotalPayments()); - if (dueAmount.compareTo(BigDecimal.ZERO) > 0) { - amountDueSection.addCell(new Paragraph(df.format(dueAmount))).setFontSize(10) - .setTextAlignment(TextAlignment.RIGHT).setFont(helvetica).setBold(); - } else { - amountDueSection.addCell(new Paragraph("0.00")).setFontSize(10).setTextAlignment(TextAlignment.RIGHT) - .setFont(helvetica).setBold(); - } - setInnerCellBorder(paymentSection, Border.NO_BORDER); - setInnerCellBorder(amountDueSection, Border.NO_BORDER); - setInnerCellBorder(totalsSection, Border.NO_BORDER); - - try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); - PdfDocument pdfDoc = new PdfDocument(new PdfWriter(bos)); - Document doc = new Document(pdfDoc, new PageSize(thermalPrinterPageSize))) { - doc.setMargins(6, 12, 2, 12); - if (logoSection != null) { - doc.add(logoSection); - } - //doc.add(addressSection); - doc.add(receiptHeader); - doc.add(divider); - doc.add(billLineItemstable); - doc.add(divider); - doc.add(totalsSection); - doc.add(divider); - doc.add(paymentSection); - doc.add(divider); - doc.add(amountDueSection); - doc.add(divider); - doc.add(new Paragraph("You were served by " + bill.getCashier().getName()).setFont(footerSectionFont) - .setFontSize(8).setTextAlignment(TextAlignment.CENTER)); - - return bos.toByteArray(); - } - catch (IOException e) { - LOG.error("Exception caught while writing PDF to stream", e); - } - - return new byte[0]; - } - - private void setInnerCellBorder(Table table, Border border) { - for (IElement child : table.getChildren()) { - if (child instanceof Cell) { - ((Cell) child).setBorder(border); - } - } - } - - private void addBillLineItem(BillLineItem item, Table table, PdfFont font) { - String itemName = ""; - if (item.getItem() != null) { - itemName = item.getItem().getDrug().getName(); - } else if (item.getBillableService() != null) { - itemName = item.getBillableService().getName(); + if (bill == null) { + throw new NullPointerException("The bill must be defined."); } - addFormattedCell(table, item.getQuantity().toString(), font, TextAlignment.LEFT); - addFormattedCell(table, itemName, font, TextAlignment.LEFT); - addFormattedCell(table, df.format(item.getPrice()), font, TextAlignment.RIGHT); - addFormattedCell(table, df.format(item.getTotal()), font, TextAlignment.RIGHT); - } - - private void addFormattedCell(Table table, String cellValue, PdfFont font, TextAlignment alignment) { - table.addCell(new Paragraph(cellValue).setTextAlignment(alignment)).setFontSize(12).setTextAlignment(alignment) - .setBorder(Border.NO_BORDER).setFont(font); + return ReceiptGenerator.createBillReceipt(bill); } + /** + * {@inheritDoc} + */ @Override - public List getAll() { - List results = super.getAll(); - removeNullLineItems(results); - return results; - } - - private void removeNullLineItems(List bills) { - if (bills == null || bills.size() == 0) { - return; - } - - for (Bill bill : bills) { - removeNullLineItems(bill); - } - } - - private void removeNullLineItems(Bill bill) { + public void purgeBill(Bill bill) { if (bill == null) { - return; - } - - // Search for any null line items (due to a bug in 1.7.0) and remove them from the line items - int index = bill.getLineItems().indexOf(null); - while (index >= 0) { - bill.getLineItems().remove(index); - - index = bill.getLineItems().indexOf(null); + throw new NullPointerException("The bill must be defined."); } + billDAO.purgeBill(bill); } + /** + * {@inheritDoc} + */ @Override - public String getVoidPrivilege() { - return PrivilegeConstants.MANAGE_BILLS; - } - - @Override - public String getSavePrivilege() { - return PrivilegeConstants.MANAGE_BILLS; - } - - @Override - public String getPurgePrivilege() { - return PrivilegeConstants.PURGE_BILLS; + public Bill voidBill(Bill bill, String voidReason) { + if (StringUtils.isBlank(voidReason)) { + throw new IllegalArgumentException("voidReason cannot be null or empty"); + } + return billDAO.saveBill(bill); } + /** + * {@inheritDoc} + */ @Override - public String getGetPrivilege() { - return PrivilegeConstants.VIEW_BILLS; + public Bill unvoidBill(Bill bill) { + return billDAO.saveBill(bill); } - public List searchBill(Patient patient) { - Criteria criteria = getRepository().createCriteria(Bill.class); - - DateTime currentDate = new DateTime(); - DateTime startOfDay = currentDate.withTimeAtStartOfDay(); - - Date startOfDayDate = startOfDay.toDate(); - - DateTime endOfDay = currentDate.plusDays(1); - endOfDay = endOfDay.withTimeAtStartOfDay(); - - Date endOfDayDate = endOfDay.toDate(); - - criteria.add(Restrictions.eq("status", BillStatus.PENDING)); - criteria.add(Restrictions.eq("patient", patient)); - criteria.add(Restrictions.ge("dateCreated", startOfDayDate)); - - criteria.add(Restrictions.lt("dateCreated", endOfDayDate)); - criteria.addOrder(Order.desc("id")); - - return criteria.list(); - } } diff --git a/api/src/main/java/org/openmrs/module/billing/api/impl/BillableItemsServiceImpl.java b/api/src/main/java/org/openmrs/module/billing/api/impl/BillableItemsServiceImpl.java index 449065ee..20a75948 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/impl/BillableItemsServiceImpl.java +++ b/api/src/main/java/org/openmrs/module/billing/api/impl/BillableItemsServiceImpl.java @@ -17,15 +17,15 @@ import org.hibernate.Criteria; import org.openmrs.module.billing.api.IBillableItemsService; -import org.openmrs.module.billing.api.base.entity.impl.BaseEntityDataServiceImpl; -import org.openmrs.module.billing.api.base.entity.security.IEntityAuthorizationPrivileges; +import org.openmrs.module.billing.api.base.entity.impl.BaseMetadataDataServiceImpl; +import org.openmrs.module.billing.api.base.entity.security.IMetadataAuthorizationPrivileges; import org.openmrs.module.billing.api.base.f.Action1; import org.openmrs.module.billing.api.model.BillableService; import org.openmrs.module.billing.api.search.BillableServiceSearch; import org.springframework.transaction.annotation.Transactional; @Transactional -public class BillableItemsServiceImpl extends BaseEntityDataServiceImpl implements IEntityAuthorizationPrivileges, IBillableItemsService { +public class BillableItemsServiceImpl extends BaseMetadataDataServiceImpl implements IMetadataAuthorizationPrivileges, IBillableItemsService { @Override public List findServices(final BillableServiceSearch serviceSearch) { @@ -39,7 +39,7 @@ public void apply(Criteria criteria) { } @Override - protected IEntityAuthorizationPrivileges getPrivileges() { + protected IMetadataAuthorizationPrivileges getPrivileges() { return this; } @@ -49,7 +49,7 @@ protected void validate(BillableService object) { } @Override - public String getVoidPrivilege() { + public String getRetirePrivilege() { return null; } diff --git a/api/src/main/java/org/openmrs/module/billing/api/impl/ICashierItemPriceServiceImpl.java b/api/src/main/java/org/openmrs/module/billing/api/impl/ICashierItemPriceServiceImpl.java index 454f8db3..4738a43f 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/impl/ICashierItemPriceServiceImpl.java +++ b/api/src/main/java/org/openmrs/module/billing/api/impl/ICashierItemPriceServiceImpl.java @@ -14,16 +14,16 @@ package org.openmrs.module.billing.api.impl; import org.openmrs.module.billing.api.ICashierItemPriceService; -import org.openmrs.module.billing.api.base.entity.impl.BaseEntityDataServiceImpl; -import org.openmrs.module.billing.api.base.entity.security.IEntityAuthorizationPrivileges; +import org.openmrs.module.billing.api.base.entity.impl.BaseMetadataDataServiceImpl; +import org.openmrs.module.billing.api.base.entity.security.IMetadataAuthorizationPrivileges; import org.openmrs.module.billing.api.model.CashierItemPrice; import org.springframework.transaction.annotation.Transactional; @Transactional -public class ICashierItemPriceServiceImpl extends BaseEntityDataServiceImpl implements IEntityAuthorizationPrivileges, ICashierItemPriceService { +public class ICashierItemPriceServiceImpl extends BaseMetadataDataServiceImpl implements IMetadataAuthorizationPrivileges, ICashierItemPriceService { @Override - protected IEntityAuthorizationPrivileges getPrivileges() { + protected IMetadataAuthorizationPrivileges getPrivileges() { return this; } @@ -33,7 +33,7 @@ protected void validate(CashierItemPrice object) { } @Override - public String getVoidPrivilege() { + public String getRetirePrivilege() { return null; } diff --git a/api/src/main/java/org/openmrs/module/billing/api/impl/ItemPriceServiceImpl.java b/api/src/main/java/org/openmrs/module/billing/api/impl/ItemPriceServiceImpl.java index 75243531..b29a2fb3 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/impl/ItemPriceServiceImpl.java +++ b/api/src/main/java/org/openmrs/module/billing/api/impl/ItemPriceServiceImpl.java @@ -21,20 +21,20 @@ import org.hibernate.criterion.Order; import org.hibernate.criterion.Restrictions; import org.openmrs.module.billing.api.ItemPriceService; -import org.openmrs.module.billing.api.base.entity.impl.BaseEntityDataServiceImpl; -import org.openmrs.module.billing.api.base.entity.security.IEntityAuthorizationPrivileges; +import org.openmrs.module.billing.api.base.entity.impl.BaseMetadataDataServiceImpl; +import org.openmrs.module.billing.api.base.entity.security.IMetadataAuthorizationPrivileges; import org.openmrs.module.billing.api.model.BillableService; import org.openmrs.module.billing.api.model.CashierItemPrice; import org.openmrs.module.stockmanagement.api.model.StockItem; import org.springframework.transaction.annotation.Transactional; @Transactional -public class ItemPriceServiceImpl extends BaseEntityDataServiceImpl implements IEntityAuthorizationPrivileges, ItemPriceService { +public class ItemPriceServiceImpl extends BaseMetadataDataServiceImpl implements IMetadataAuthorizationPrivileges, ItemPriceService { private static final Log LOG = LogFactory.getLog(ItemPriceServiceImpl.class); @Override - protected IEntityAuthorizationPrivileges getPrivileges() { + protected IMetadataAuthorizationPrivileges getPrivileges() { return this; } @@ -44,13 +44,13 @@ protected void validate(CashierItemPrice object) { } @Override - public CashierItemPrice save(CashierItemPrice object) { + public CashierItemPrice saveBill(CashierItemPrice object) { LOG.debug("Processing save Price"); - return super.save(object); + return super.saveBill(object); } @Override - public String getVoidPrivilege() { + public String getRetirePrivilege() { return null; } diff --git a/api/src/main/java/org/openmrs/module/billing/api/model/Bill.java b/api/src/main/java/org/openmrs/module/billing/api/model/Bill.java index 205253df..87aa19f5 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/model/Bill.java +++ b/api/src/main/java/org/openmrs/module/billing/api/model/Bill.java @@ -15,12 +15,13 @@ import java.math.BigDecimal; import java.security.AccessControlException; -import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; +import lombok.Getter; +import lombok.Setter; import org.openmrs.BaseOpenmrsData; import org.openmrs.Patient; import org.openmrs.Provider; @@ -32,9 +33,11 @@ * Model class that represents a list of {@link BillLineItem}s and {@link Payment}s created by a * cashier for a patient. */ +@Getter +@Setter public class Bill extends BaseOpenmrsData { - public static final long serialVersionUID = 0L; + private static final long serialVersionUID = 0L; private Integer billId; @@ -60,29 +63,10 @@ public class Bill extends BaseOpenmrsData { private String adjustmentReason; - public String getAdjustmentReason() { - return adjustmentReason; - } - - public void setAdjustmentReason(String adjustmentReason) { - this.adjustmentReason = adjustmentReason; - } - - public Boolean isReceiptPrinted() { - return receiptPrinted; - } - - public void setReceiptPrinted(Boolean receiptPrinted) { - this.receiptPrinted = receiptPrinted; - } - - public Boolean getReceiptPrinted() { - return receiptPrinted; - } - public BigDecimal getTotal() { BigDecimal total = BigDecimal.ZERO; + List lineItems = getLineItems(); if (lineItems != null) { for (BillLineItem line : lineItems) { if (line != null && !line.getVoided()) { @@ -97,6 +81,7 @@ public BigDecimal getTotal() { public BigDecimal getTotalPayments() { BigDecimal total = BigDecimal.ZERO; + Set payments = getPayments(); if (payments != null) { for (Payment payment : payments) { if (payment != null && !payment.getVoided()) { @@ -108,59 +93,17 @@ public BigDecimal getTotalPayments() { return total; } - public BigDecimal getAmountPaid() { - BigDecimal total = getTotal(); - BigDecimal totalPayments = getTotalPayments(); - - return total.min(totalPayments); - } - @Override public Integer getId() { - return billId; + return this.getBillId(); } @Override public void setId(Integer id) { - billId = id; - } - - public String getReceiptNumber() { - return receiptNumber; - } - - public void setReceiptNumber(String number) { - this.receiptNumber = number; - } - - public Provider getCashier() { - return cashier; - } - - public void setCashier(Provider cashier) { - this.cashier = cashier; - } - - public Patient getPatient() { - return patient; - } - - public void setPatient(Patient patient) { - this.patient = patient; - } - - public CashPoint getCashPoint() { - return cashPoint; - } - - public void setCashPoint(CashPoint cashPoint) { - this.cashPoint = cashPoint; - } - - public Bill getBillAdjusted() { - return billAdjusted; + this.setBillId(id); } + // Custom setter - updates adjusted bill status public void setBillAdjusted(Bill billAdjusted) { this.billAdjusted = billAdjusted; @@ -169,32 +112,6 @@ public void setBillAdjusted(Bill billAdjusted) { } } - public BillStatus getStatus() { - return status; - } - - public void setStatus(BillStatus status) { - this.status = status; - } - - public List getLineItems() { - return lineItems; - } - - public void setLineItems(List lineItems) { - this.lineItems = lineItems; - } - - public BillLineItem addLineItem(StockItem item, CashierItemPrice price, int quantity) { - if (item == null) { - throw new NullPointerException("The item to add must be defined."); - } - if (price == null) { - throw new NullPointerException("The item price must be defined."); - } - return addLineItem(item, price.getPrice(), "", quantity); - } - public BillLineItem addLineItem(StockItem item, BigDecimal price, String priceName, int quantity) { if (item == null) { throw new IllegalArgumentException("The item to add must be defined."); @@ -221,7 +138,7 @@ public void addLineItem(BillLineItem item) { } if (this.lineItems == null) { - this.lineItems = new ArrayList(); + this.lineItems = new ArrayList<>(); } this.lineItems.add(item); @@ -236,48 +153,13 @@ public void removeLineItem(BillLineItem item) { } } - public Set getPayments() { - return payments; - } - - public void setPayments(Set payments) { - this.payments = payments; - } - - public Payment addPayment(PaymentMode mode, Set attributes, BigDecimal amount, - BigDecimal amountTendered) { - if (mode == null) { - throw new NullPointerException("The payment mode must be defined."); - } - if (amount == null) { - throw new NullPointerException(("The payment amount must be defined.")); - } - - Payment payment = new Payment(); - payment.setInstanceType(mode); - payment.setAmount(amount); - payment.setAmountTendered(amountTendered); - - if (attributes != null && attributes.size() > 0) { - payment.setAttributes(attributes); - - for (PaymentAttribute attribute : attributes) { - attribute.setOwner(payment); - } - } - - addPayment(payment); - - return payment; - } - public void addPayment(Payment payment) { if (payment == null) { throw new NullPointerException("The payment to add must be defined."); } if (this.payments == null) { - this.payments = new HashSet(); + this.payments = new HashSet<>(); } this.payments.add(payment); @@ -287,7 +169,7 @@ public void addPayment(Payment payment) { } public void synchronizeBillStatus() { - if (this.getPayments().size() > 0 && getTotalPayments().compareTo(BigDecimal.ZERO) > 0) { + if (!this.getPayments().isEmpty() && getTotalPayments().compareTo(BigDecimal.ZERO) > 0) { boolean billFullySettled = getTotalPayments().compareTo(getTotal()) >= 0; if (billFullySettled) { this.setStatus(BillStatus.PAID); @@ -303,14 +185,6 @@ public void removePayment(Payment payment) { } } - public Set getAdjustedBy() { - return adjustedBy; - } - - public void setAdjustedBy(Set adjustedBy) { - this.adjustedBy = adjustedBy; - } - public void addAdjustedBy(Bill adjustedBill) { checkAuthorizedToAdjust(); if (adjustedBill == null) { @@ -318,25 +192,31 @@ public void addAdjustedBy(Bill adjustedBill) { } if (this.adjustedBy == null) { - this.adjustedBy = new HashSet(); + this.adjustedBy = new HashSet<>(); } adjustedBill.setBillAdjusted(this); this.adjustedBy.add(adjustedBill); } - public void removeAdjustedBy(Bill adjustedBill) { - if (adjustedBill != null && this.adjustedBy != null) { - this.adjustedBy.remove(adjustedBill); - } - } - private void checkAuthorizedToAdjust() { if (!Context.hasPrivilege(PrivilegeConstants.ADJUST_BILLS)) { throw new AccessControlException("Access denied to adjust bill."); } } + /** + * Checks if the bill is in PENDING state. + * + * @return {@code true} if the bill is new (no ID) or is in PENDING state, {@code false} otherwise + */ + public boolean editable() { + // New bills (no ID) are considered pending, existing bills must be in PENDING state + // If we do a partial payment bill is set to POSTED status. We should be able to edit posted status too + return getStatus() == null || this.getId() == null || this.getStatus() == BillStatus.PENDING + || this.getStatus() == BillStatus.POSTED; + } + public void recalculateLineItemOrder() { int orderCounter = 0; for (BillLineItem lineItem : this.getLineItems()) { @@ -344,12 +224,4 @@ public void recalculateLineItemOrder() { } } - public String getLastUpdated() { - SimpleDateFormat ft = Context.getDateTimeFormat(); - String changedStr = (this.getDateChanged() != null) ? ft.format(this.getDateChanged()) : null; - String createdStr = (this.getDateCreated() != null) ? ft.format(this.getDateCreated()) : ""; - String dateString = (changedStr != null) ? changedStr : createdStr; - - return dateString; - } } diff --git a/api/src/main/java/org/openmrs/module/billing/api/model/BillExemption.java b/api/src/main/java/org/openmrs/module/billing/api/model/BillExemption.java new file mode 100644 index 00000000..e4f1168e --- /dev/null +++ b/api/src/main/java/org/openmrs/module/billing/api/model/BillExemption.java @@ -0,0 +1,82 @@ +package org.openmrs.module.billing.api.model; + +import org.openmrs.BaseOpenmrsMetadata; +import org.openmrs.Concept; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; +import javax.persistence.Table; +import java.util.List; + +@Entity +@Table(name = "bill_exemption") +public class BillExemption extends BaseOpenmrsMetadata { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "exemption_id") + private Integer exemptionId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "concept_id", nullable = false) + private Concept concept; + + @Enumerated(EnumType.STRING) + @Column(name = "exemption_type", nullable = false) + private ExemptionType exemptionType; + + @OneToMany(mappedBy = "billExemption", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) + private List rules; + + @Override + public Integer getId() { + return exemptionId; + } + + @Override + public void setId(Integer exemptionId) { + this.exemptionId = exemptionId; + } + + public Integer getExemptionId() { + return exemptionId; + } + + public void setExemptionId(Integer exemptionId) { + this.exemptionId = exemptionId; + } + + public Concept getConcept() { + return concept; + } + + public void setConcept(Concept concept) { + this.concept = concept; + } + + public ExemptionType getExemptionType() { + return exemptionType; + } + + public void setExemptionType(ExemptionType exemptionType) { + this.exemptionType = exemptionType; + } + + public List getRules() { + return rules; + } + + public void setRules(List rules) { + this.rules = rules; + } +} diff --git a/api/src/main/java/org/openmrs/module/billing/api/model/BillExemptionRule.java b/api/src/main/java/org/openmrs/module/billing/api/model/BillExemptionRule.java new file mode 100644 index 00000000..d4b8cd32 --- /dev/null +++ b/api/src/main/java/org/openmrs/module/billing/api/model/BillExemptionRule.java @@ -0,0 +1,78 @@ +package org.openmrs.module.billing.api.model; + +import org.openmrs.BaseOpenmrsData; +import org.openmrs.module.billing.api.evaluator.ScriptType; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +@Entity +@Table(name = "bill_exemption_rule") +public class BillExemptionRule extends BaseOpenmrsData { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "rule_id") + private Integer ruleId; + + @Enumerated(EnumType.STRING) + @Column(name = "script_type", nullable = false) + private ScriptType scriptType; + + @Column(name = "script", nullable = false) + private String script; + + @ManyToOne + @JoinColumn(name = "exemption_id") + private BillExemption billExemption; + + @Override + public Integer getId() { + return getRuleId(); + } + + @Override + public void setId(Integer id) { + setRuleId(id); + } + + public Integer getRuleId() { + return ruleId; + } + + public ScriptType getScriptType() { + return scriptType; + } + + public void setScriptType(ScriptType scriptType) { + this.scriptType = scriptType; + } + + public void setRuleId(Integer ruleId) { + this.ruleId = ruleId; + } + + public String getScript() { + return script; + } + + public void setScript(String script) { + this.script = script; + } + + public BillExemption getBillingExemption() { + return billExemption; + } + + public void setBillingExemption(BillExemption billExemption) { + this.billExemption = billExemption; + } +} diff --git a/api/src/main/java/org/openmrs/module/billing/api/model/BillLineItem.java b/api/src/main/java/org/openmrs/module/billing/api/model/BillLineItem.java index 07c8d84b..60cedc09 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/model/BillLineItem.java +++ b/api/src/main/java/org/openmrs/module/billing/api/model/BillLineItem.java @@ -14,8 +14,9 @@ package org.openmrs.module.billing.api.model; import java.math.BigDecimal; +import java.util.Objects; -import org.openmrs.BaseOpenmrsData; +import org.openmrs.BaseChangeableOpenmrsData; import org.openmrs.Order; import org.openmrs.module.stockmanagement.api.model.StockItem; @@ -23,9 +24,9 @@ * A LineItem represents a line on a {@link Bill} which will bill some quantity of a particular * {@link StockItem}. */ -public class BillLineItem extends BaseOpenmrsData { +public class BillLineItem extends BaseChangeableOpenmrsData { - public static final long serialVersionUID = 0L; + private static final long serialVersionUID = 0L; private int billLineItemId; diff --git a/api/src/main/java/org/openmrs/module/billing/api/model/BillableService.java b/api/src/main/java/org/openmrs/module/billing/api/model/BillableService.java index f8098ca6..10a3b7e8 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/model/BillableService.java +++ b/api/src/main/java/org/openmrs/module/billing/api/model/BillableService.java @@ -16,12 +16,12 @@ import java.util.ArrayList; import java.util.List; -import org.openmrs.BaseOpenmrsData; +import org.openmrs.BaseChangeableOpenmrsMetadata; import org.openmrs.Concept; import org.openmrs.Location; import org.openmrs.Provider; -public class BillableService extends BaseOpenmrsData { +public class BillableService extends BaseChangeableOpenmrsMetadata { public static final long serialVersionUID = 0L; diff --git a/api/src/main/java/org/openmrs/module/billing/api/model/CashierItemPrice.java b/api/src/main/java/org/openmrs/module/billing/api/model/CashierItemPrice.java index 7023e831..43656eb4 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/model/CashierItemPrice.java +++ b/api/src/main/java/org/openmrs/module/billing/api/model/CashierItemPrice.java @@ -15,10 +15,10 @@ import java.math.BigDecimal; -import org.openmrs.BaseOpenmrsData; +import org.openmrs.BaseChangeableOpenmrsMetadata; import org.openmrs.module.stockmanagement.api.model.StockItem; -public class CashierItemPrice extends BaseOpenmrsData { +public class CashierItemPrice extends BaseChangeableOpenmrsMetadata { public static final long serialVersionUID = 0L; diff --git a/api/src/main/java/org/openmrs/module/billing/api/model/ExemptionType.java b/api/src/main/java/org/openmrs/module/billing/api/model/ExemptionType.java new file mode 100644 index 00000000..508d3225 --- /dev/null +++ b/api/src/main/java/org/openmrs/module/billing/api/model/ExemptionType.java @@ -0,0 +1,7 @@ +package org.openmrs.module.billing.api.model; + +public enum ExemptionType { + SERVICE, + COMMODITY, + BOTH +} diff --git a/api/src/main/java/org/openmrs/module/billing/api/search/BillSearch.java b/api/src/main/java/org/openmrs/module/billing/api/search/BillSearch.java index 7c64d112..7e4c79cc 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/search/BillSearch.java +++ b/api/src/main/java/org/openmrs/module/billing/api/search/BillSearch.java @@ -13,46 +13,36 @@ */ package org.openmrs.module.billing.api.search; -import org.hibernate.Criteria; -import org.hibernate.criterion.Order; -import org.hibernate.criterion.Restrictions; -import org.openmrs.module.billing.api.base.entity.search.BaseDataTemplateSearch; -import org.openmrs.module.billing.api.model.Bill; +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.openmrs.module.billing.api.model.BillStatus; /** - * A search template class for the {@link Bill} model. + * A search criteria holder for {@link org.openmrs.module.billing.api.model.Bill} queries. This + * class holds search parameters that are used by the DAO layer to build queries. Uses Lombok's + * builder pattern for fluent API. */ -public class BillSearch extends BaseDataTemplateSearch { +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class BillSearch { + + private String patientUuid; + + private String cashierUuid; + + private String cashPointUuid; - public BillSearch() { - this(new Bill(), false); - } + private List statuses; - public BillSearch(Bill template) { - this(template, false); - } + private String patientName; - public BillSearch(Bill template, Boolean includeRetired) { - super(template, includeRetired); - } + private Boolean includeVoided = false; - @Override - public void updateCriteria(Criteria criteria) { - super.updateCriteria(criteria); - - Bill bill = getTemplate(); - if (bill.getCashier() != null) { - criteria.add(Restrictions.eq("cashier", bill.getCashier())); - } - if (bill.getCashPoint() != null) { - criteria.add(Restrictions.eq("cashPoint", bill.getCashPoint())); - } - if (bill.getPatient() != null) { - criteria.add(Restrictions.eq("patient", bill.getPatient())); - } - if (bill.getStatus() != null) { - criteria.add(Restrictions.eq("status", bill.getStatus())); - } - criteria.addOrder(Order.desc("id")); - } + private Boolean includeVoidedLineItems = false; } diff --git a/api/src/main/java/org/openmrs/module/billing/api/search/BillableServiceSearch.java b/api/src/main/java/org/openmrs/module/billing/api/search/BillableServiceSearch.java index 7891d066..3e08ea09 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/search/BillableServiceSearch.java +++ b/api/src/main/java/org/openmrs/module/billing/api/search/BillableServiceSearch.java @@ -16,10 +16,10 @@ import org.hibernate.Criteria; import org.hibernate.criterion.MatchMode; import org.hibernate.criterion.Restrictions; -import org.openmrs.module.billing.api.base.entity.search.BaseDataTemplateSearch; +import org.openmrs.module.billing.api.base.entity.search.BaseMetadataTemplateSearch; import org.openmrs.module.billing.api.model.BillableService; -public class BillableServiceSearch extends BaseDataTemplateSearch { +public class BillableServiceSearch extends BaseMetadataTemplateSearch { public BillableServiceSearch() { this(new BillableService(), false); diff --git a/api/src/main/java/org/openmrs/module/billing/api/util/PrivilegeConstants.java b/api/src/main/java/org/openmrs/module/billing/api/util/PrivilegeConstants.java index 04c0c2fb..28012c8c 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/util/PrivilegeConstants.java +++ b/api/src/main/java/org/openmrs/module/billing/api/util/PrivilegeConstants.java @@ -42,6 +42,8 @@ public class PrivilegeConstants { public static final String PURGE_BILLS = "Purge Cashier Bills"; + public static final String DELETE_BILLS = "Delete Cashier Bills"; + public static final String REFUND_MONEY = "Refund Money"; public static final String REPRINT_RECEIPT = "Reprint Receipt"; diff --git a/api/src/main/java/org/openmrs/module/billing/exemptions/BillingExemptionChecker.java b/api/src/main/java/org/openmrs/module/billing/exemptions/BillingExemptionChecker.java deleted file mode 100644 index 58fe6ceb..00000000 --- a/api/src/main/java/org/openmrs/module/billing/exemptions/BillingExemptionChecker.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.openmrs.module.billing.exemptions; - -import java.util.Set; - -public class BillingExemptionChecker { - - /** - * Checks if a given concept ID is exempted from billing under the provided category. - * - * @param category The category to check (e.g., "services" or "commodities") - * @param key The specific key within the category (e.g., "program:HIV") - * @param conceptId The concept ID to check for exemption - * @return true if the concept ID is exempted, false otherwise - */ - public boolean isExempted(String category, String key, Integer conceptId) { - Set exemptedConcepts; - if (category.equals("services")) { - exemptedConcepts = BillingExemptions.SERVICES.get(key); - } else if (category.equals("commodities")) { - exemptedConcepts = BillingExemptions.COMMODITIES.get(key); - } else { - return false; - } - return exemptedConcepts != null && exemptedConcepts.contains(conceptId); - } -} diff --git a/api/src/main/java/org/openmrs/module/billing/exemptions/BillingExemptions.java b/api/src/main/java/org/openmrs/module/billing/exemptions/BillingExemptions.java deleted file mode 100644 index 5045dd11..00000000 --- a/api/src/main/java/org/openmrs/module/billing/exemptions/BillingExemptions.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * The contents of this file are subject to the OpenMRS Public License - * Version 1.1 (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * http://license.openmrs.org - * - * Software distributed under the License is distributed on an "AS IS" - * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the - * License for the specific language governing rights and limitations - * under the License. - * - * Copyright (C) OpenMRS, LLC. All Rights Reserved. - */ -package org.openmrs.module.billing.exemptions; - -import java.util.Map; -import java.util.Set; - -/** - * An object with details of services and commodities exempted from billing. The class variables - * should be populated once on startup and should live in memory during application use. If this - * list grows big, we should think of a separate way to keep this. - */ -public abstract class BillingExemptions { - - /** - * Should contain a list of unique concept ids which are exempted from billing. The convention is to - * have keys that map to a set of concept ids. This is not prescriptive and should be up to an - * implementation to define which convention works A sample payload can look like the below: { - * "services": { "all": [111,112,113], "program:HIV": [211,212,213], "program:TB": [220,220,220], - * "age<5": [311,312,313], "visitAttribute:prisoner": [711,712,713] }, "commodities": { "all": - * [511,512,513], "program:HIV": [611,612,613] } } Please note that the key can be anything, as long - * as the implementation takes care of the evaluation logic There should be a separate logic to - * populate the services and commodities and a separate one to check for exemptions and bill - * appropriately TODO: make the implementation as generic as possible - */ - public static Map> SERVICES; - - public static Map> COMMODITIES; - - public abstract void buildBillingExemptionList(); - - public static void setSERVICES(Map> SERVICES) { - BillingExemptions.SERVICES = SERVICES; - } - - public static void setCOMMODITIES(Map> COMMODITIES) { - BillingExemptions.COMMODITIES = COMMODITIES; - } -} diff --git a/api/src/main/java/org/openmrs/module/billing/exemptions/BillingExemptionsConfig.java b/api/src/main/java/org/openmrs/module/billing/exemptions/BillingExemptionsConfig.java deleted file mode 100644 index 91d25de4..00000000 --- a/api/src/main/java/org/openmrs/module/billing/exemptions/BillingExemptionsConfig.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.openmrs.module.billing.exemptions; - -import java.util.Map; -import java.util.Set; - -public class BillingExemptionsConfig { - - private Map> services; - - private Map> commodities; - - // Getters and setters - public Map> getServices() { - return services; - } - - public void setServices(Map> services) { - this.services = services; - } - - public Map> getCommodities() { - return commodities; - } - - public void setCommodities(Map> commodities) { - this.commodities = commodities; - } -} diff --git a/api/src/main/java/org/openmrs/module/billing/exemptions/DefaultBillingExemptions.java b/api/src/main/java/org/openmrs/module/billing/exemptions/DefaultBillingExemptions.java deleted file mode 100644 index 3d69273a..00000000 --- a/api/src/main/java/org/openmrs/module/billing/exemptions/DefaultBillingExemptions.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.openmrs.module.billing.exemptions; - -import com.fasterxml.jackson.databind.ObjectMapper; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import java.io.File; -import java.io.IOException; - -public class DefaultBillingExemptions extends BillingExemptions { - - private static final Log LOG = LogFactory.getLog(DefaultBillingExemptions.class); - - private static final String CONFIG_FILE_PATH = "/billing/exemptions/SampleBillingExemptions.json"; - - @Override - public void buildBillingExemptionList() { - ObjectMapper mapper = new ObjectMapper(); - try { - BillingExemptionsConfig config = mapper.readValue(new File(CONFIG_FILE_PATH), BillingExemptionsConfig.class); - - setSERVICES(config.getServices()); - setCOMMODITIES(config.getCommodities()); - - } - catch (IOException e) { - LOG.error("Failed to load billing exemptions from " + CONFIG_FILE_PATH + ": " + e.getMessage()); - throw new RuntimeException("Unable to load billing exemptions", e); - } - } -} diff --git a/api/src/main/java/org/openmrs/module/billing/exemptions/SampleBillingExemptionBuilder.java b/api/src/main/java/org/openmrs/module/billing/exemptions/SampleBillingExemptionBuilder.java deleted file mode 100644 index 1e7b35d0..00000000 --- a/api/src/main/java/org/openmrs/module/billing/exemptions/SampleBillingExemptionBuilder.java +++ /dev/null @@ -1,126 +0,0 @@ -package org.openmrs.module.billing.exemptions; - -import org.apache.commons.lang3.StringUtils; -import org.codehaus.jackson.JsonNode; -import org.codehaus.jackson.map.ObjectMapper; -import org.codehaus.jackson.node.ArrayNode; -import org.codehaus.jackson.node.ObjectNode; -import org.openmrs.GlobalProperty; -import org.openmrs.api.context.Context; -import org.openmrs.module.billing.api.util.CashierModuleConstants; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.*; - -/* - * Builds a list of exemptions from json file -*/ -public class SampleBillingExemptionBuilder extends BillingExemptions { - - private static final Log LOG = LogFactory.getLog(SampleBillingExemptionBuilder.class); - - public SampleBillingExemptionBuilder() { - } - - @Override - public void buildBillingExemptionList() { - GlobalProperty gpConfiguredFilePath = Context.getAdministrationService() - .getGlobalPropertyObject(CashierModuleConstants.BILLING_EXEMPTIONS_CONFIG_FILE_PATH); - if (gpConfiguredFilePath == null || StringUtils.isBlank(gpConfiguredFilePath.getPropertyValue())) { - try { - initializeExemptionsConfig(); - } - catch (Exception e) { - LOG.error("Billing exemptions have not been configured...", e); - } - return; - } - String configurationFilePath = gpConfiguredFilePath.getPropertyValue(); - FileInputStream fileInputStream; - ObjectNode config = null; - try { - fileInputStream = new FileInputStream(configurationFilePath); - } - catch (FileNotFoundException e) { - e.printStackTrace(); - try { - initializeExemptionsConfig(); - } - catch (Exception ex) { - LOG.error("The configuration file for billing exemptions was found, but could not be processed", ex); - } - return; - } - - if (fileInputStream != null) { - ObjectMapper mapper = new ObjectMapper(); - try { - config = mapper.readValue(fileInputStream, ObjectNode.class); - } - catch (IOException e) { - e.printStackTrace(); - try { - initializeExemptionsConfig(); - } - catch (Exception ex) { - LOG.error( - "The configuration file for billing exemptions was found, but could not be understood. Check that the JSON object is well formed", - ex); - } - return; - } - } - - if (config != null) { - ObjectNode configuredServices = (ObjectNode) config.get("services"); - ObjectNode commodities = (ObjectNode) config.get("commodities"); - - if (configuredServices != null) { - Map> exemptedServices = mapConcepts(configuredServices); - BillingExemptions.setSERVICES(exemptedServices); - } - - if (commodities != null) { - Map> exemptedCommodities = mapConcepts(commodities); - BillingExemptions.setCOMMODITIES(exemptedCommodities); - } - } else { - initializeExemptionsConfig(); - } - } - - private Map> mapConcepts(ObjectNode node) { - Map> exemptionList = new HashMap<>(); - if (node != null) { - Iterator> iterator = node.getFields(); - iterator.forEachRemaining(entry -> { - Set conceptSet = new HashSet<>(); - String key = entry.getKey(); - ArrayNode conceptIds = (ArrayNode) entry.getValue(); - if (conceptIds.isArray() && conceptIds.size() > 0) { - for (int i = 0; i < conceptIds.size(); i++) { - try { - conceptSet.add(conceptIds.get(i).getIntValue()); - } - catch (Exception e) { - LOG.error("Error converting concept ID to integer: " + conceptIds.get(i).toString(), e); - } - } - } - if (conceptSet.size() > 0) { - exemptionList.put(key, conceptSet); - } - }); - } - return exemptionList; - } - - private void initializeExemptionsConfig() { - BillingExemptions.setCOMMODITIES(new HashMap<>()); - BillingExemptions.setSERVICES(new HashMap<>()); - } -} diff --git a/api/src/main/java/org/openmrs/module/billing/exemptions/SampleBillingExemptions.json b/api/src/main/java/org/openmrs/module/billing/exemptions/SampleBillingExemptions.json deleted file mode 100644 index 678841c1..00000000 --- a/api/src/main/java/org/openmrs/module/billing/exemptions/SampleBillingExemptions.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "services" : { - "all" : [ - { - "id" : "167410AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", - "description" : "Clinical Consultation" - }, - { - "id" : "160542AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", - "description" : "Outpatient Department" - } - ], - "program:HIV" : [ - { - "id" : "855e254f-a5db-4760-88b3-26c3d0cdda14", - "description" : "HIV Consultation" - } - ], - "program:TB" : [ - { - "id" : "855e254f-a5db-4760-88b3-26c3d0cdda14", - "description" : "TB Treatment" - } - ], - "age<5" : [ - { - "id" : "160537AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", - "description" : "Pediatric Consultation" - }, - { - "id" : "1283AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", - "description" : "Labaratory Orders" - } - ] - }, - "commodities" : { - "all" : [ - { - "id" : "164103AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", - "description" : "General Commodity" - } - ], - "program:HIV" : [ - { - "id" : "161187AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", - "description" : "HIV Test Kits" - } - ] - } -} diff --git a/api/src/main/java/org/openmrs/module/billing/util/ReceiptGenerator.java b/api/src/main/java/org/openmrs/module/billing/util/ReceiptGenerator.java new file mode 100644 index 00000000..8acd19b3 --- /dev/null +++ b/api/src/main/java/org/openmrs/module/billing/util/ReceiptGenerator.java @@ -0,0 +1,285 @@ +package org.openmrs.module.billing.util; + +import com.itextpdf.io.font.constants.StandardFonts; +import com.itextpdf.io.image.ImageDataFactory; +import com.itextpdf.kernel.font.PdfFont; +import com.itextpdf.kernel.font.PdfFontFactory; +import com.itextpdf.kernel.geom.PageSize; +import com.itextpdf.kernel.geom.Rectangle; +import com.itextpdf.kernel.pdf.PdfDocument; +import com.itextpdf.kernel.pdf.PdfWriter; +import com.itextpdf.layout.Document; +import com.itextpdf.layout.borders.Border; +import com.itextpdf.layout.element.Cell; +import com.itextpdf.layout.element.IElement; +import com.itextpdf.layout.element.Image; +import com.itextpdf.layout.element.Paragraph; +import com.itextpdf.layout.element.Table; +import com.itextpdf.layout.element.Text; +import com.itextpdf.layout.properties.TextAlignment; +import com.itextpdf.layout.properties.UnitValue; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.text.WordUtils; +import org.openmrs.Patient; +import org.openmrs.api.context.Context; +import org.openmrs.module.billing.api.model.Bill; +import org.openmrs.module.billing.api.model.BillLineItem; +import org.openmrs.module.billing.api.model.Payment; +import org.openmrs.util.ConfigUtil; +import org.openmrs.util.OpenmrsClassLoader; +import org.openmrs.util.OpenmrsUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.math.BigDecimal; +import java.net.MalformedURLException; +import java.net.URL; +import java.text.NumberFormat; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; + +public class ReceiptGenerator { + + private static final Logger LOG = LoggerFactory.getLogger(ReceiptGenerator.class); + + private static final String GP_BILL_LOGO_PATH = "billing.receipt.logoPath"; + + //TODO: Try to clean this up more + public static byte[] createBillReceipt(Bill bill) { + NumberFormat nf = NumberFormat.getCurrencyInstance(Context.getLocale()); + DateTimeFormatter dateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT) + .withLocale(Context.getLocale()); + + Patient patient = bill.getPatient(); + String fullName = patient.getPersonName().getFullName(); + String gender = patient.getGender() != null ? patient.getGender() : ""; + String dob = patient.getBirthdate() != null ? dateFormatter.format(patient.getBirthdate().toInstant()) : ""; + + /** + * https://kb.itextpdf.com/home/it7kb/faq/how-to-set-the-page-size-to-envelope-size-with-landscape-orientation + * page size: 3.5inch length, 1.1 inch height 1mm = 0.0394 inch length = 450mm = 17.7165 inch = + * 127.5588 points height = 300mm = 11.811 inch = 85.0392 points The measurement system in PDF + * doesn't use inches, but user units. By default, 1 user unit = 1 point, and 1 inch = 72 points. + * Thermal printer: 4 x 10 inches paper 4 inches = 4 x 72 = 288 5 inches = 10 x 72 = 720 + */ + int FONT_SIZE_12 = 12; + Rectangle thermalPrinterPageSize = new Rectangle(288, 720); + + PdfFont timesRoman; + PdfFont courierBold; + PdfFont helvetica; + PdfFont helveticaBold; + try { + timesRoman = PdfFontFactory.createFont(StandardFonts.TIMES_ROMAN); + courierBold = PdfFontFactory.createFont(StandardFonts.COURIER_BOLD); + helvetica = PdfFontFactory.createFont(StandardFonts.HELVETICA); + helveticaBold = PdfFontFactory.createFont(StandardFonts.HELVETICA_BOLD); + + } + catch (IOException e) { + throw new RuntimeException(e); + } + + PdfFont headerSectionFont = helveticaBold; + PdfFont billItemSectionFont = helvetica; + PdfFont footerSectionFont = courierBold; + URL logoUrl = null; + + String logoPath = ConfigUtil.getGlobalProperty(GP_BILL_LOGO_PATH); + if (StringUtils.isNotBlank(logoPath)) { + File file = new File(logoPath.trim()); + if (!file.isAbsolute()) { + file = new File(OpenmrsUtil.getApplicationDataDirectory(), logoPath.trim()); + } + + if (file.exists()) { + try { + logoUrl = file.getAbsoluteFile().toURI().toURL(); + } + catch (MalformedURLException e) { + LOG.error("Error Loading file: {}", file.getAbsoluteFile(), e); + } + } + } + + if (logoUrl == null) { + logoUrl = OpenmrsClassLoader.getInstance().getResource("img/openmrs-logo.png"); + } + + Image logoImage = null; + if (logoUrl != null) { + logoImage = new Image(ImageDataFactory.create(logoUrl)); + logoImage.scaleToFit(80, 80); + } + Paragraph divider = new Paragraph("------------------------------------------------------------------"); + Text billDateLabel = new Text(Utils.getSimpleDateFormat("dd-MMM-yyyy HH:mm:ss").format(bill.getDateCreated())); + + Paragraph logoSection = null; + if (logoImage != null) { + logoSection = new Paragraph(); + logoSection.setFontSize(14); + logoSection.add(logoImage).add("\n"); + logoSection.setTextAlignment(TextAlignment.CENTER); + logoSection.setFont(timesRoman).setBold(); + } + + float[] headerColWidth = { 2f, 7f }; + Table receiptHeader = new Table(headerColWidth); + receiptHeader.setWidth(UnitValue.createPercentValue(100f)); + + receiptHeader.addCell(new Paragraph("Date:")).setFontSize(FONT_SIZE_12).setTextAlignment(TextAlignment.LEFT) + .setFont(headerSectionFont); + receiptHeader.addCell(new Paragraph(billDateLabel.getText())).setFontSize(FONT_SIZE_12) + .setTextAlignment(TextAlignment.LEFT).setFont(helvetica); + + receiptHeader.addCell(new Paragraph("Receipt No:")).setFontSize(FONT_SIZE_12).setTextAlignment(TextAlignment.LEFT) + .setFont(headerSectionFont); + receiptHeader.addCell(new Paragraph(bill.getReceiptNumber())).setFontSize(FONT_SIZE_12) + .setTextAlignment(TextAlignment.LEFT).setFont(helvetica); + + receiptHeader.addCell(new Paragraph("Patient:")).setFontSize(FONT_SIZE_12).setTextAlignment(TextAlignment.LEFT) + .setFont(headerSectionFont); + receiptHeader.addCell(new Paragraph(WordUtils.capitalizeFully(fullName))).setFontSize(FONT_SIZE_12) + .setTextAlignment(TextAlignment.LEFT).setFont(helvetica); + + receiptHeader.addCell(new Paragraph("Gender:")).setFontSize(FONT_SIZE_12).setTextAlignment(TextAlignment.LEFT) + .setFont(headerSectionFont); + receiptHeader.addCell(new Paragraph(WordUtils.capitalizeFully(gender))).setFontSize(FONT_SIZE_12) + .setTextAlignment(TextAlignment.LEFT).setFont(helvetica); + + receiptHeader.addCell(new Paragraph("Date of Birth:")).setFontSize(FONT_SIZE_12).setTextAlignment(TextAlignment.LEFT) + .setFont(headerSectionFont); + receiptHeader.addCell(new Paragraph(WordUtils.capitalizeFully(dob))).setFontSize(FONT_SIZE_12) + .setTextAlignment(TextAlignment.LEFT).setFont(helvetica); + + float[] columnWidths = { 1f, 5f, 2f, 2f }; + Table billLineItemstable = new Table(columnWidths); + billLineItemstable.setBorder(Border.NO_BORDER); + billLineItemstable.setWidth(UnitValue.createPercentValue(100f)); + + billLineItemstable.addCell(new Paragraph("Qty").setTextAlignment(TextAlignment.LEFT)).setFontSize(FONT_SIZE_12) + .setTextAlignment(TextAlignment.LEFT); + billLineItemstable.addCell(new Paragraph("Item").setTextAlignment(TextAlignment.LEFT)).setFontSize(FONT_SIZE_12) + .setTextAlignment(TextAlignment.LEFT); + billLineItemstable.addCell(new Paragraph("Price")).setFontSize(FONT_SIZE_12).setTextAlignment(TextAlignment.RIGHT); + billLineItemstable.addCell(new Paragraph("Total")).setFontSize(FONT_SIZE_12).setTextAlignment(TextAlignment.RIGHT); + + for (BillLineItem item : bill.getLineItems()) { + if (item.getVoided()) { + continue; + } + + addBillLineItem(item, billLineItemstable, billItemSectionFont, nf); + } + + float[] totalColWidth = { 1f, 5f, 2f, 2f }; + Table totalsSection = new Table(totalColWidth); + totalsSection.setWidth(UnitValue.createPercentValue(100f)); + + totalsSection.addCell(new Paragraph(" ")); + totalsSection.addCell(new Paragraph(" ")); + totalsSection.addCell(new Paragraph("Total")).setFontSize(10).setTextAlignment(TextAlignment.RIGHT) + .setFont(helvetica).setBold(); + totalsSection.addCell(new Paragraph(nf.format(bill.getTotal()))).setFontSize(10) + .setTextAlignment(TextAlignment.RIGHT).setFont(helvetica).setBold(); + + setInnerCellBorder(receiptHeader, Border.NO_BORDER); + setInnerCellBorder(billLineItemstable, Border.NO_BORDER); + + float[] paymentColWidth = { 1f, 5f, 2f, 2f }; + Table paymentSection = new Table(paymentColWidth); + paymentSection.setWidth(UnitValue.createPercentValue(100f)); + paymentSection.addCell(new Paragraph(" ")); + paymentSection.addCell(new Paragraph(" ")); + paymentSection.addCell(new Paragraph("Payment").setTextAlignment(TextAlignment.RIGHT).setBold()); + paymentSection.addCell(new Paragraph("")); + // append payment rows + for (Payment payment : bill.getPayments()) { + paymentSection.addCell(new Paragraph(" ")); + paymentSection.addCell(new Paragraph(" ")); + paymentSection.addCell(new Paragraph(payment.getInstanceType().getName()).setTextAlignment(TextAlignment.RIGHT)) + .setFontSize(10).setFont(helvetica); + paymentSection + .addCell(new Paragraph(nf.format(payment.getAmountTendered())).setTextAlignment(TextAlignment.RIGHT)) + .setFontSize(10).setFont(helvetica); + } + + float[] amountDueColWidth = { 1f, 5f, 2f, 2f }; + Table amountDueSection = new Table(amountDueColWidth); + amountDueSection.setWidth(UnitValue.createPercentValue(100f)); + + amountDueSection.addCell(new Paragraph(" ")); + amountDueSection.addCell(new Paragraph(" ")); + + amountDueSection.addCell(new Paragraph("Due Amount")).setFontSize(10).setTextAlignment(TextAlignment.RIGHT) + .setFont(helvetica).setBold(); + BigDecimal dueAmount = bill.getTotal().subtract(bill.getTotalPayments()); + if (dueAmount.compareTo(BigDecimal.ZERO) > 0) { + amountDueSection.addCell(new Paragraph(nf.format(dueAmount))).setFontSize(10) + .setTextAlignment(TextAlignment.RIGHT).setFont(helvetica).setBold(); + } else { + amountDueSection.addCell(new Paragraph("0.00")).setFontSize(10).setTextAlignment(TextAlignment.RIGHT) + .setFont(helvetica).setBold(); + } + setInnerCellBorder(paymentSection, Border.NO_BORDER); + setInnerCellBorder(amountDueSection, Border.NO_BORDER); + setInnerCellBorder(totalsSection, Border.NO_BORDER); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try (PdfDocument pdfDoc = new PdfDocument(new PdfWriter(bos)); + Document doc = new Document(pdfDoc, new PageSize(thermalPrinterPageSize))) { + doc.setMargins(6, 12, 2, 12); + if (logoSection != null) { + doc.add(logoSection); + } + //doc.add(addressSection); + doc.add(receiptHeader); + doc.add(divider); + doc.add(billLineItemstable); + doc.add(divider); + doc.add(totalsSection); + doc.add(divider); + doc.add(paymentSection); + doc.add(divider); + doc.add(amountDueSection); + doc.add(divider); + doc.add(new Paragraph("You were served by " + bill.getCashier().getName()).setFont(footerSectionFont) + .setFontSize(8).setTextAlignment(TextAlignment.CENTER)); + } + catch (Exception e) { + LOG.error("Exception caught while writing PDF to stream", e); + return bos.toByteArray(); + } + + return bos.toByteArray(); + } + + private static void setInnerCellBorder(Table table, Border border) { + for (IElement child : table.getChildren()) { + if (child instanceof Cell) { + ((Cell) child).setBorder(border); + } + } + } + + private static void addBillLineItem(BillLineItem item, Table table, PdfFont font, NumberFormat nf) { + String itemName = ""; + if (item.getItem() != null) { + itemName = item.getItem().getDrug().getName(); + } else if (item.getBillableService() != null) { + itemName = item.getBillableService().getName(); + } + addFormattedCell(table, item.getQuantity().toString(), font, TextAlignment.LEFT); + addFormattedCell(table, itemName, font, TextAlignment.LEFT); + addFormattedCell(table, nf.format(item.getPrice()), font, TextAlignment.RIGHT); + addFormattedCell(table, nf.format(item.getTotal()), font, TextAlignment.RIGHT); + } + + private static void addFormattedCell(Table table, String cellValue, PdfFont font, TextAlignment alignment) { + table.addCell(new Paragraph(cellValue).setTextAlignment(alignment)).setFontSize(12).setTextAlignment(alignment) + .setBorder(Border.NO_BORDER).setFont(font); + } +} diff --git a/api/src/main/java/org/openmrs/module/billing/util/Utils.java b/api/src/main/java/org/openmrs/module/billing/util/Utils.java index ad125e43..1fe88811 100644 --- a/api/src/main/java/org/openmrs/module/billing/util/Utils.java +++ b/api/src/main/java/org/openmrs/module/billing/util/Utils.java @@ -40,13 +40,9 @@ import org.openmrs.Concept; import org.openmrs.Encounter; import org.openmrs.EncounterType; -import org.openmrs.GlobalProperty; -import org.openmrs.Location; -import org.openmrs.LocationAttribute; import org.openmrs.Obs; import org.openmrs.Patient; import org.openmrs.api.context.Context; -import org.openmrs.util.PrivilegeConstants; public class Utils { diff --git a/api/src/main/java/org/openmrs/module/billing/validator/BillValidator.java b/api/src/main/java/org/openmrs/module/billing/validator/BillValidator.java new file mode 100644 index 00000000..e71f9b45 --- /dev/null +++ b/api/src/main/java/org/openmrs/module/billing/validator/BillValidator.java @@ -0,0 +1,44 @@ +package org.openmrs.module.billing.validator; + +import org.apache.commons.lang3.StringUtils; +import org.openmrs.annotation.Handler; +import org.openmrs.api.context.Context; +import org.openmrs.module.billing.api.BillService; +import org.openmrs.module.billing.api.model.Bill; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; + +@Handler(supports = { Bill.class }, order = 50) +public class BillValidator implements Validator { + + @Override + public boolean supports(Class clazz) { + return Bill.class.isAssignableFrom(clazz); + } + + @Override + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void validate(Object target, Errors errors) { + if (!(target instanceof Bill)) { + errors.reject("error.general"); + } else { + Bill bill = (Bill) target; + + if (bill.getVoided() && StringUtils.isBlank(bill.getVoidReason())) { + errors.rejectValue("voided", "error.null"); + } + + if (bill.getId() != null) { + Bill existingBill = Context.getService(BillService.class).getBill(bill.getBillId()); + if (existingBill != null && !existingBill.editable()) { + errors.reject("billing.bill.notEditable", + "Bill can only be modified when the bill is in PENDING state. Current status: " + + existingBill.getStatus()); + } + } + } + } + +} diff --git a/api/src/main/resources/Bill.hbm.xml b/api/src/main/resources/Bill.hbm.xml index 597b9c06..18560dc5 100644 --- a/api/src/main/resources/Bill.hbm.xml +++ b/api/src/main/resources/Bill.hbm.xml @@ -29,10 +29,10 @@ - - - - + + + + @@ -61,6 +61,7 @@ + org.openmrs.module.billing.api.model.BillableServiceStatus @@ -74,11 +75,10 @@ - - - - - + + + + @@ -112,10 +112,12 @@
+ + diff --git a/api/src/main/resources/moduleApplicationContext.xml b/api/src/main/resources/moduleApplicationContext.xml index 5be1c805..9d90e4c0 100644 --- a/api/src/main/resources/moduleApplicationContext.xml +++ b/api/src/main/resources/moduleApplicationContext.xml @@ -21,14 +21,6 @@
- - - - org.openmrs.module.billing.api.IBillService - - - - @@ -94,6 +86,8 @@ + + @@ -115,16 +109,6 @@ - - - - - - - - - - @@ -210,4 +194,65 @@ class="org.openmrs.module.billing.api.base.entity.db.hibernate.BaseHibernateRepositoryImpl"> + + + + + org.openmrs.module.billing.api.BillService + + + + + + + + + org.openmrs.module.billing.api.BillExemptionService + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/api/src/test/java/org/openmrs/module/billing/IBillServiceTest.java b/api/src/test/java/org/openmrs/module/billing/IBillServiceTest.java deleted file mode 100644 index 81af64e5..00000000 --- a/api/src/test/java/org/openmrs/module/billing/IBillServiceTest.java +++ /dev/null @@ -1,464 +0,0 @@ -///* -// * The contents of this file are subject to the OpenMRS Public License -// * Version 1.1 (the "License"); you may not use this file except in -// * compliance with the License. You may obtain a copy of the License at -// * http://license.openmrs.org -// * -// * Software distributed under the License is distributed on an "AS IS" -// * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the -// * License for the specific language governing rights and limitations -// * under the License. -// * -// * Copyright (C) OpenMRS, LLC. All Rights Reserved. -// */ -//package org.openmrs.module.cashier.api; -// -//import java.math.BigDecimal; -//import java.util.Iterator; -//import java.util.List; -//import java.util.Set; -// -////import liquibase.util.StringUtils; -// -//import org.junit.Assert; -//import org.junit.Test; -//import org.openmrs.Patient; -//import org.openmrs.api.PatientService; -//import org.openmrs.api.ProviderService; -//import org.openmrs.api.context.Context; -//import org.openmrs.module.cashier.api.base.PagingInfo; -//import org.openmrs.module.cashier.api.model.Bill; -//import org.openmrs.module.cashier.api.model.BillLineItem; -//import org.openmrs.module.cashier.api.model.BillStatus; -//import org.openmrs.module.cashier.api.model.CashPoint; -//import org.openmrs.module.cashier.api.model.Payment; -//import org.openmrs.module.cashier.api.model.PaymentAttribute; -//import org.openmrs.module.cashier.api.model.PaymentMode; -//import org.openmrs.module.cashier.api.search.BillSearch; -//import org.openmrs.module.cashier.api.base.entity.IEntityDataServiceTest; -//import org.openmrs.module.openhmis.inventory.api.IItemDataService; -//import org.openmrs.module.openhmis.inventory.api.IItemDataServiceTest; -//import org.openmrs.module.openhmis.inventory.api.model.Item; -// -//public abstract class IBillServiceTest extends IEntityDataServiceTest { -// public static final String BILL_DATASET = TestConstants.BASE_DATASET_DIR + "BillTest.xml"; -// -// private ProviderService providerService; -// private PatientService patientService; -// private IItemDataService itemService; -// private IPaymentModeService paymentModeService; -// private IPaymentModeAttributeTypeService paymentModeAttributeTypeService; -// private ICashPointService cashPointService; -// -// @Override -// public void before() throws Exception { -// super.before(); -// -// providerService = Context.getProviderService(); -// patientService = Context.getPatientService(); -// itemService = Context.getService(IItemDataService.class); -// paymentModeService = Context.getService(IPaymentModeService.class); -// paymentModeAttributeTypeService = Context.getService(IPaymentModeAttributeTypeService.class); -// cashPointService = Context.getService(ICashPointService.class); -// -// executeDataSet(IItemDataServiceTest.ITEM_DATASET); -// executeDataSet(IPaymentModeServiceTest.PAYMENT_MODE_DATASET); -// executeDataSet(ICashPointServiceTest.CASH_POINT_DATASET); -// executeDataSet(TestConstants.CORE_DATASET); -// executeDataSet(BILL_DATASET); -// } -// -// @Override -// public Bill createEntity(boolean valid) { -// Bill bill = new Bill(); -// -// if (valid) { -// bill.setCashier(providerService.getProvider(0)); -// bill.setPatient(patientService.getPatient(0)); -// bill.setCashPoint(cashPointService.getById(0)); -// bill.setReceiptNumber("Test 1234"); -// bill.setStatus(BillStatus.PAID); -// } -// -// Item item = itemService.getById(0); -// bill.addLineItem(item, item.getPrices().iterator().next(), 1); -// item = itemService.getById(1); -// bill.addLineItem(item, item.getPrices().iterator().next(), 1); -// -// PaymentMode mode = paymentModeService.getById(0); -// Payment payment = bill.addPayment(mode, null, BigDecimal.valueOf(100), BigDecimal.valueOf(100)); -// payment.addAttribute(paymentModeAttributeTypeService.getById(0), "test"); -// payment.addAttribute(paymentModeAttributeTypeService.getById(1), "test2"); -// payment.addAttribute(paymentModeAttributeTypeService.getById(2), "test3"); -// -// mode = paymentModeService.getById(1); -// bill.addPayment(mode, null, BigDecimal.valueOf(200), BigDecimal.valueOf(200)); -// -// return bill; -// } -// -// @Override -// protected int getTestEntityCount() { -// return 1; -// } -// -// @Override -// protected void updateEntityFields(Bill bill) { -// bill.setCashier(providerService.getProvider(1)); -// bill.setPatient(patientService.getPatient(2)); -// bill.setCashPoint(cashPointService.getById(0)); -// bill.setReceiptNumber(bill.getReceiptNumber() + " updated"); -// bill.setStatus(BillStatus.PENDING); -// -// List lines = bill.getLineItems(); -// if (lines.size() > 0) { -// BillLineItem item = lines.get(0); -// -// item.setPrice(item.getPrice().multiply(BigDecimal.valueOf(2))); -// item.setPriceName(item.getPriceName() + " updated"); -// -// if (lines.size() > 1) { -// item = lines.get(1); -// -// bill.removeLineItem(item); -// } -// } -// -// Item newItem = itemService.getById(2); -// bill.addLineItem(newItem, newItem.getPrices().iterator().next(), 3); -// -// Set payments = bill.getPayments(); -// if (payments.size() > 0) { -// Iterator iterator = payments.iterator(); -// -// Payment payment = iterator.next(); -// payment.setAmount(payment.getAmount().divide(BigDecimal.valueOf(2))); -// -// if (payments.size() > 1) { -// payment = iterator.next(); -// -// bill.removePayment(payment); -// } -// } -// -// bill.addPayment(paymentModeService.getById(2), null, BigDecimal.valueOf(303.11), BigDecimal.valueOf(350.00)); -// } -// -// @Override -// protected void assertEntity(Bill expected, Bill actual) { -// super.assertEntity(expected, actual); -// -// Assert.assertNotNull(expected.getCashier()); -// Assert.assertNotNull(actual.getCashier()); -// Assert.assertEquals(expected.getCashier().getId(), actual.getCashier().getId()); -// Assert.assertNotNull(expected.getPatient()); -// Assert.assertNotNull(actual.getPatient()); -// Assert.assertEquals(expected.getPatient().getId(), actual.getPatient().getId()); -// Assert.assertNotNull(expected.getCashPoint()); -// Assert.assertNotNull(actual.getCashPoint()); -// Assert.assertEquals(expected.getCashPoint().getId(), actual.getCashPoint().getId()); -// -// Assert.assertEquals(expected.getReceiptNumber(), actual.getReceiptNumber()); -// Assert.assertEquals(expected.getStatus(), actual.getStatus()); -// -// if (expected.getLineItems() == null) { -// Assert.assertNull(actual.getLineItems()); -// } else { -// Assert.assertEquals(expected.getLineItems().size(), actual.getLineItems().size()); -// BillLineItem[] expectedItems = new BillLineItem[expected.getLineItems().size()]; -// expected.getLineItems().toArray(expectedItems); -// BillLineItem[] actualItems = new BillLineItem[actual.getLineItems().size()]; -// actual.getLineItems().toArray(actualItems); -// for (int i = 0; i < expected.getLineItems().size(); i++) { -// Assert.assertEquals(expectedItems[i].getId(), actualItems[i].getId()); -// Assert.assertEquals(expectedItems[i].getItem(), actualItems[i].getItem()); -// Assert.assertEquals(expectedItems[i].getPrice(), actualItems[i].getPrice()); -// Assert.assertEquals(expectedItems[i].getPriceName(), actualItems[i].getPriceName()); -// Assert.assertEquals(expectedItems[i].getQuantity(), actualItems[i].getQuantity()); -// Assert.assertEquals(expectedItems[i].getUuid(), actualItems[i].getUuid()); -// } -// } -// -// if (expected.getPayments() == null) { -// Assert.assertNull(actual.getPayments()); -// } else { -// Assert.assertEquals(expected.getPayments().size(), actual.getPayments().size()); -// Payment[] expectedPayments = new Payment[expected.getPayments().size()]; -// expected.getPayments().toArray(expectedPayments); -// Payment[] actualPayments = new Payment[actual.getPayments().size()]; -// actual.getPayments().toArray(actualPayments); -// for (int i = 0; i < expected.getPayments().size(); i++) { -// Assert.assertEquals(expectedPayments[i].getId(), actualPayments[i].getId()); -// Assert.assertEquals(expectedPayments[i].getInstanceType(), actualPayments[i].getInstanceType()); -// Assert.assertEquals(expectedPayments[i].getAmount(), actualPayments[i].getAmount()); -// Assert.assertEquals(expectedPayments[i].getUuid(), actualPayments[i].getUuid()); -// -// if (expectedPayments[i].getAttributes() == null) { -// Assert.assertNull(actualPayments[i].getAttributes()); -// } else { -// Assert.assertEquals(expectedPayments[i].getAttributes().size(), actualPayments[i].getAttributes() -// .size()); -// if (expectedPayments[i].getAttributes().size() > 0) { -// PaymentAttribute[] expectedAttributes = -// new PaymentAttribute[expectedPayments[i].getAttributes().size()]; -// expectedPayments[i].getAttributes().toArray(expectedAttributes); -// PaymentAttribute[] actualAttributes = -// new PaymentAttribute[actualPayments[i].getAttributes().size()]; -// actualPayments[i].getAttributes().toArray(actualAttributes); -// for (int j = 0; j < expectedAttributes.length; j++) { -// Assert.assertEquals(expectedAttributes[j].getId(), actualAttributes[j].getId()); -// Assert.assertEquals(expectedAttributes[j].getValue(), actualAttributes[j].getValue()); -// Assert.assertEquals(expectedAttributes[j].getAttributeType(), -// actualAttributes[j].getAttributeType()); -// Assert.assertEquals(expectedAttributes[j].getUuid(), actualAttributes[j].getUuid()); -// } -// } -// } -// } -// } -// } -// -// /** -// * @verifies throw IllegalArgumentException if the receipt number is null -// * @see IBillService#getBillByReceiptNumber(String) -// */ -// @Test(expected = IllegalArgumentException.class) -// public void getBillByReceiptNumber_shouldThrowIllegalArgumentExceptionIfTheReceiptNumberIsNull() throws Exception { -// service.getBillByReceiptNumber(null); -// } -// -// /** -// * @verifies throw IllegalArgumentException if the receipt number is empty -// * @see IBillService#getBillByReceiptNumber(String) -// */ -// @Test(expected = IllegalArgumentException.class) -// public void getBillByReceiptNumber_shouldThrowIllegalArgumentExceptionIfTheReceiptNumberIsEmpty() throws Exception { -// service.getBillByReceiptNumber(""); -// } -// -// /** -// * @verifies throw IllegalArgumentException if the receipt number is longer than 255 characters -// * @see IBillService#getBillByReceiptNumber(String) -// */ -// @Test(expected = IllegalArgumentException.class) -// public void getBillByReceiptNumber_shouldThrowIllegalArgumentExceptionIfTheReceiptNumberIsLongerThan255Characters() -// throws Exception { -// // service.getBillByReceiptNumber(StringUtils.repeat("A", 256)); -// } -// -// /** -// * @verifies return the bill with the specified reciept number -// * @see IBillService#getBillByReceiptNumber(String) -// */ -// @Test -// public void getBillByReceiptNumber_shouldReturnTheBillWithTheSpecifiedRecieptNumber() throws Exception { -// Bill bill = service.getBillByReceiptNumber("test 1 receipt number"); -// Assert.assertNotNull(bill); -// -// Bill expected = service.getById(0); -// -// assertEntity(expected, bill); -// } -// -// /** -// * @verifies return null if the receipt number is not found -// * @see IBillService#getBillByReceiptNumber(String) -// */ -// @Test -// public void getBillByReceiptNumber_shouldReturnNullIfTheReceiptNumberIsNotFound() throws Exception { -// Bill bill = service.getBillByReceiptNumber("not a valid number"); -// -// Assert.assertNull(bill); -// } -// -// @Test -// public void save_adjustedBill() throws Exception { -// Bill bill = createEntity(true); -// bill.setBillAdjusted(service.getById(0)); -// service.save(bill); -// -// Context.flushSession(); -// -// bill = service.getById(bill.getId()); -// Assert.assertNotNull(bill); -// Assert.assertNotNull(bill.getBillAdjusted()); -// -// Bill adjustedBill = service.getById(bill.getBillAdjusted().getId()); -// Assert.assertNotNull(adjustedBill); -// Assert.assertEquals(BillStatus.ADJUSTED, adjustedBill.getStatus()); -// Assert.assertTrue(adjustedBill.getAdjustedBy().size() > 0); -// -// boolean foundAdjustor = false; -// for (Bill adjustor : adjustedBill.getAdjustedBy()) { -// if (adjustor.getId() == bill.getId()) { -// foundAdjustor = true; -// break; -// } -// } -// -// Assert.assertTrue("Could not find the adjusting bill.", foundAdjustor); -// } -// -// /** -// * @verifies throw NullPointerException if patient is null -// * @see IBillService#getBillsByPatient(org.openmrs.Patient, PagingInfo) -// */ -// @Test(expected = NullPointerException.class) -// public void getBillsByPatient_shouldThrowNullPointerExceptionIfPatientIsNull() throws Exception { -// service.getBillsByPatient(null, null); -// } -// -// /** -// * @verifies return all bills for the specified patient -// * @see IBillService#getBillsByPatient(org.openmrs.Patient, PagingInfo) -// */ -// @Test -// public void getBillsByPatientId_shouldReturnAllBillsForTheSpecifiedPatient() throws Exception { -// Patient patient = patientService.getPatient(0); -// -// List bills = service.getBillsByPatient(patient, null); -// -// Assert.assertNotNull(bills); -// Assert.assertEquals(1, bills.size()); -// assertEntity(service.getById(0), bills.get(0)); -// -// bills = service.getBillsByPatientId(patient.getId(), null); -// Assert.assertNotNull(bills); -// Assert.assertEquals(1, bills.size()); -// assertEntity(service.getById(0), bills.get(0)); -// } -// -// /** -// * @verifies return an empty list if the specified patient has no bills -// * @see IBillService#getBillsByPatient(org.openmrs.Patient, PagingInfo) -// */ -// @Test -// public void getBillsByPatientId_shouldReturnAnEmptyListIfTheSpecifiedPatientHasNoBills() throws Exception { -// Patient patient = patientService.getPatient(1); -// -// List bills = service.getBillsByPatient(patient, null); -// Assert.assertNotNull(bills); -// Assert.assertEquals(0, bills.size()); -// -// bills = service.getBillsByPatientId(1, null); -// Assert.assertNotNull(bills); -// Assert.assertEquals(0, bills.size()); -// } -// -// /** -// * @verifies throw IllegalArgumentException if the patientId is less than zero -// * @see IBillService#getBillsByPatientId(int, PagingInfo) -// */ -// @Test(expected = IllegalArgumentException.class) -// public void getBillsByPatientId_shouldThrowIllegalArgumentExceptionIfThePatientIdIsLessThanZero() throws Exception { -// service.getBillsByPatientId(-1, null); -// } -// -// /** -// * @verifies throw NullPointerException if bill search is null -// * @see IBillService#getBills(BillSearch, PagingInfo) -// */ -// @Test(expected = NullPointerException.class) -// public void getBills_throwNullPointerExceptionIfBillSearchIsNull() throws Exception { -// service.getBills(null, null); -// } -// -// /** -// * @verifies throw NullPointerException if bill search template object is null -// * @see IBillService#getBills(BillSearch, PagingInfo) -// */ -// @Test(expected = NullPointerException.class) -// public void getBills_throwNullPointerExceptionIfBillSearchTemplateObjectIsNull() throws Exception { -// BillSearch search = new BillSearch(); -// search.setTemplate(null); -// service.getBills(search, null); -// } -// -// /** -// * @verifies return an empty list if no bills are found via the search -// * @see IBillService#getBills(BillSearch, PagingInfo) -// */ -// @Test -// public void getBills_returnAnEmptyListIfNoBillsAreFoundViaTheSearch() throws Exception { -// BillSearch billSearch = new BillSearch(); -// Bill bill = new Bill(); -// CashPoint cashPoint = new CashPoint(); -// cashPoint.setId(2); -// bill.setCashPoint(cashPoint); -// billSearch.setTemplate(bill); -// List results = service.getBills(billSearch, null); -// Assert.assertTrue(results.isEmpty()); -// } -// -// /** -// * @verifies return bills filtered by cashier -// * @see IBillService#getBills(BillSearch, PagingInfo) -// */ -// @Test -// public void getBills_returnBillsFilteredByCashier() throws Exception { -// Bill bill = new Bill(); -// bill.setCashier(providerService.getProvider(0)); -// List results = service.getBills(new BillSearch(bill), null); -// Assert.assertEquals(1, results.size()); -// } -// -// /** -// * @verifies return bills filtered by cash point -// * @see IBillService#getBills(BillSearch, PagingInfo) -// */ -// @Test -// public void getBills_returnBillsFilteredByCashPoint() throws Exception { -// Bill bill = new Bill(); -// bill.setCashPoint(cashPointService.getById(0)); -// List results = service.getBills(new BillSearch(bill), null); -// Assert.assertEquals(1, results.size()); -// } -// -// /** -// * @verifies return bills filtered by patient -// * @see IBillService#getBills(BillSearch, PagingInfo) -// */ -// @Test -// public void getBills_returnBillsFilteredByPatient() throws Exception { -// Bill bill = new Bill(); -// bill.setPatient(patientService.getPatient(0)); -// List results = service.getBills(new BillSearch(bill), null); -// Assert.assertEquals(1, results.size()); -// } -// -// /** -// * @verifies return bills filtered by status -// * @see IBillService#getBills(BillSearch, PagingInfo) -// */ -// @Test -// public void getBills_returnBillsFilteredByStatus() throws Exception { -// Bill bill = new Bill(); -// bill.setStatus(BillStatus.POSTED); -// List results = service.getBills(new BillSearch(bill), null); -// Assert.assertEquals(1, results.size()); -// } -// -// /** -// * @verifies return all bills if paging is null -// * @see IBillService#getBills(BillSearch, PagingInfo) -// */ -// @Test -// public void getBills_returnAllBillsIfPagingIsNull() throws Exception { -// List results = service.getBills(new BillSearch(), null); -// Assert.assertEquals(1, results.size()); -// } -// -// /** -// * @verifies return paged bills if paging is specified -// * @see IBillService#getBills(BillSearch, org.openmrs.module.cashier.api.base.PagingInfo) -// */ -// @Test -// public void getBills_returnPagedBillsIfPagingIsSpecified() throws Exception { -// PagingInfo pagingInfo = new PagingInfo(1, 100); -// List results = service.getBills(new BillSearch(), pagingInfo); -// -// Assert.assertNotNull(results); -// Assert.assertEquals(1, results.size()); -// Assert.assertEquals(1, (long)pagingInfo.getTotalRecordCount()); -// } -//} diff --git a/api/src/test/java/org/openmrs/module/billing/ICashPointServiceTest.java b/api/src/test/java/org/openmrs/module/billing/ICashPointServiceTest.java index 42fc20d7..75875a62 100644 --- a/api/src/test/java/org/openmrs/module/billing/ICashPointServiceTest.java +++ b/api/src/test/java/org/openmrs/module/billing/ICashPointServiceTest.java @@ -105,7 +105,7 @@ public void getCashPointsByLocation_shouldNotReturnRetiredCashpointsUnlessSpecif CashPoint cashPoint = service.getById(0); cashPoint.setRetired(true); cashPoint.setRetireReason("reason"); - service.save(cashPoint); + service.saveBill(cashPoint); Location location = Context.getLocationService().getLocation(0); Context.flushSession(); @@ -188,7 +188,7 @@ public void getCashPointsByLocationAndName_shouldNotReturnRetiredCashpointsUnles CashPoint cashPoint = service.getById(0); cashPoint.setRetired(true); cashPoint.setRetireReason("reason"); - service.save(cashPoint); + service.saveBill(cashPoint); Location location = Context.getLocationService().getLocation(0); Context.flushSession(); diff --git a/api/src/test/java/org/openmrs/module/billing/ITimesheetServiceTest.java b/api/src/test/java/org/openmrs/module/billing/ITimesheetServiceTest.java index 9cd21963..54ed7acf 100644 --- a/api/src/test/java/org/openmrs/module/billing/ITimesheetServiceTest.java +++ b/api/src/test/java/org/openmrs/module/billing/ITimesheetServiceTest.java @@ -123,7 +123,7 @@ public void getCurrentTimesheet_shouldReturnTheCurrentTimesheetForTheCashier() t Timesheet timesheet = createEntity(true); timesheet.setClockOut(null); - timesheet = service.save(timesheet); + timesheet = service.saveBill(timesheet); Context.flushSession(); Timesheet current = service.getCurrentTimesheet(timesheet.getCashier()); @@ -161,7 +161,7 @@ public void getCurrentTimesheet_shouldReturnTheMostRecentTimesheetIfTheCashierIs timesheet.setCashier(cashier); timesheet.setClockOut(null); - service.save(timesheet); + service.saveBill(timesheet); Context.flushSession(); Timesheet current = service.getCurrentTimesheet(cashier); diff --git a/api/src/test/java/org/openmrs/module/billing/SequentialReceiptNumberGeneratorTest.java b/api/src/test/java/org/openmrs/module/billing/SequentialReceiptNumberGeneratorTest.java index 863a7992..1b585fc8 100644 --- a/api/src/test/java/org/openmrs/module/billing/SequentialReceiptNumberGeneratorTest.java +++ b/api/src/test/java/org/openmrs/module/billing/SequentialReceiptNumberGeneratorTest.java @@ -14,19 +14,16 @@ package org.openmrs.module.billing; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.powermock.api.mockito.PowerMockito.mockStatic; - -import java.text.SimpleDateFormat; -import java.util.Calendar; -import java.util.Date; +import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; -import org.junit.runner.RunWith; +import org.mockito.MockedStatic; import org.openmrs.Provider; import org.openmrs.api.context.Context; import org.openmrs.module.billing.api.ISequentialReceiptNumberGeneratorService; @@ -35,32 +32,29 @@ import org.openmrs.module.billing.api.model.CashPoint; import org.openmrs.module.billing.api.model.SequentialReceiptNumberGeneratorModel; import org.openmrs.patient.impl.LuhnIdentifierValidator; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; -@RunWith(PowerMockRunner.class) -@PrepareForTest({ Context.class, SequentialReceiptNumberGenerator.class }) public class SequentialReceiptNumberGeneratorTest { private ISequentialReceiptNumberGeneratorService service; private SequentialReceiptNumberGenerator generator; - private Calendar calendar; + private MockedStatic contextMock; @Before public void before() { - mockStatic(Context.class); + contextMock = mockStatic(Context.class); service = mock(ISequentialReceiptNumberGeneratorService.class); - when(Context.getService(ISequentialReceiptNumberGeneratorService.class)).thenReturn(service); - - mockStatic(Calendar.class); - calendar = mock(Calendar.class); - when(Calendar.getInstance()).thenReturn(calendar); + contextMock.when(() -> Context.getService(ISequentialReceiptNumberGeneratorService.class)).thenReturn(service); generator = new SequentialReceiptNumberGenerator(); } + @After + public void tearDown() { + contextMock.close(); + } + /** * @verifies Create a new receipt number by grouping type * @see SequentialReceiptNumberGenerator#generateNumber(Bill) @@ -143,23 +137,24 @@ public void generateNumber_shouldCreateANewReceiptNumberBySequenceType() throws generator.load(); when(service.reserveNextSequence("")).thenReturn(52013); - Date date = new Date(125, 0, 1, 13, 14, 15); - SimpleDateFormat format = new SimpleDateFormat("yyMMdd"); - when(calendar.getTimeInMillis()).thenReturn(date.getTime()); - number = generator.generateNumber(bill); Assert.assertNotNull(number); - Assert.assertEquals(format.format(date) + "52013", number); + // Should end with the sequence number + Assert.assertTrue(number.endsWith("52013")); + // Should be longer than just the sequence due to date prefix (yyMMdd = 6 chars + 5 digits) + Assert.assertEquals(11, number.length()); + // Test DATE_TIME_COUNTER sequence type model.setSequenceType(SequentialReceiptNumberGenerator.SequenceType.DATE_TIME_COUNTER); generator.load(); when(service.reserveNextSequence("")).thenReturn(15); - format = new SimpleDateFormat("yyMMddHHmmss"); - number = generator.generateNumber(bill); Assert.assertNotNull(number); - Assert.assertEquals(format.format(date) + "0015", number); + // Should end with the sequence number + Assert.assertTrue(number.endsWith("0015")); + // Should be longer than just the sequence due to date-time prefix (yyMMddHHmmss = 12 chars + 4 digits) + Assert.assertEquals(16, number.length()); } /** @@ -186,26 +181,31 @@ public void generateNumber_shouldCreateANewReceiptNumberUsingTheSpecifiedSeparat Assert.assertNotNull(number); Assert.assertEquals("0001", number); + // Test separator with DATE_TIME_COUNTER model.setGroupingType(SequentialReceiptNumberGenerator.GroupingType.CASHIER_AND_CASH_POINT); model.setSequenceType(SequentialReceiptNumberGenerator.SequenceType.DATE_TIME_COUNTER); generator.load(); when(service.reserveNextSequence("P1CP3")).thenReturn(52013); - Date date = new Date(125, 0, 1, 13, 14, 15); - SimpleDateFormat format = new SimpleDateFormat("yyMMddHHmmss"); - when(calendar.getTimeInMillis()).thenReturn(date.getTime()); - number = generator.generateNumber(bill); Assert.assertNotNull(number); - Assert.assertEquals("P1-CP3-" + format.format(date) + "52013", number); + // Should start with grouping and separator + Assert.assertTrue(number.startsWith("P1-CP3-")); + // Should end with the sequence number + Assert.assertTrue(number.endsWith("52013")); model.setIncludeCheckDigit(true); generator.load(); number = generator.generateNumber(bill); Assert.assertNotNull(number); - String expected = "P1-CP3-" + format.format(date) + "52013"; - Assert.assertEquals(expected + "-" + generator.generateCheckDigit(expected), number); + // Should start with grouping and separator + Assert.assertTrue(number.startsWith("P1-CP3-")); + // Should contain the sequence number before the check digit + Assert.assertTrue(number.contains("52013")); + // Should end with a check digit (single digit after final separator) + String[] parts = number.split("-"); + Assert.assertEquals(1, parts[parts.length - 1].length()); } /** diff --git a/api/src/test/java/org/openmrs/module/billing/TestConstants.java b/api/src/test/java/org/openmrs/module/billing/TestConstants.java index 9595cdc7..52b84680 100644 --- a/api/src/test/java/org/openmrs/module/billing/TestConstants.java +++ b/api/src/test/java/org/openmrs/module/billing/TestConstants.java @@ -18,4 +18,6 @@ public class TestConstants { public static final String BASE_DATASET_DIR = "org/openmrs/module/billing/api/include/"; public static final String CORE_DATASET = BASE_DATASET_DIR + "CoreTest.xml"; + + public static final String CORE_DATASET2 = BASE_DATASET_DIR + "CoreTest-2.0.xml"; } diff --git a/api/src/test/java/org/openmrs/module/billing/api/db/hibernate/BillExemptionDAOImplTest.java b/api/src/test/java/org/openmrs/module/billing/api/db/hibernate/BillExemptionDAOImplTest.java new file mode 100644 index 00000000..8d33147d --- /dev/null +++ b/api/src/test/java/org/openmrs/module/billing/api/db/hibernate/BillExemptionDAOImplTest.java @@ -0,0 +1,296 @@ +/* + * The contents of this file are subject to the OpenMRS Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://license.openmrs.org + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the + * License for the specific language governing rights and limitations + * under the License. + * + * Copyright (C) OpenMRS, LLC. All Rights Reserved. + */ + +package org.openmrs.module.billing.api.db.hibernate; + +import org.hibernate.SessionFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openmrs.BaseOpenmrsMetadata; +import org.openmrs.Concept; +import org.openmrs.api.ConceptService; +import org.openmrs.api.context.Context; +import org.openmrs.module.billing.TestConstants; +import org.openmrs.module.billing.api.db.BillExemptionDAO; +import org.openmrs.module.billing.api.model.BillExemption; +import org.openmrs.module.billing.api.model.BillExemptionRule; +import org.openmrs.module.billing.api.model.ExemptionType; +import org.openmrs.test.jupiter.BaseModuleContextSensitiveTest; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.Date; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class BillExemptionDAOImplTest extends BaseModuleContextSensitiveTest { + + private static final String EXEMPTION_UUID_1 = "3386610d-d272-43a9-9083-6c2a5272ade9"; + + private BillExemptionDAO dao; + + private ConceptService conceptService; + + @Autowired + private SessionFactory sessionFactory; + + @BeforeEach + public void setup() { + dao = new BillExemptionDAOImpl(sessionFactory); + conceptService = Context.getConceptService(); + executeDataSet(TestConstants.CORE_DATASET2); + executeDataSet(TestConstants.BASE_DATASET_DIR + "BillExemptionTest.xml"); + } + + /** + * @see BillExemptionDAO#getBillingExemptionById(Integer) + */ + @Test + public void getBillingExemptionById_shouldReturnExemptionWithSpecifiedId() { + BillExemption exemption = dao.getBillingExemptionById(1); + + assertNotNull(exemption); + assertEquals(1, exemption.getExemptionId()); + assertEquals("Service Exemption 1", exemption.getName()); + assertEquals(ExemptionType.SERVICE, exemption.getExemptionType()); + assertFalse(exemption.getRetired()); + } + + /** + * @see BillExemptionDAO#getBillingExemptionById(Integer) + */ + @Test + public void getBillingExemptionById_shouldReturnNullForInvalidId() { + BillExemption exemption = dao.getBillingExemptionById(999); + + assertNull(exemption); + } + + /** + * @see BillExemptionDAO#getBillingExemptionByUuid(String) + */ + @Test + public void getBillingExemptionByUuid_shouldReturnExemptionWithSpecifiedUuid() { + BillExemption exemption = dao.getBillingExemptionByUuid(EXEMPTION_UUID_1); + + assertNotNull(exemption); + assertEquals(EXEMPTION_UUID_1, exemption.getUuid()); + assertEquals("Service Exemption 1", exemption.getName()); + assertEquals(ExemptionType.SERVICE, exemption.getExemptionType()); + } + + /** + * @see BillExemptionDAO#getExemptionsByConcept(Concept, ExemptionType, boolean) + */ + @Test + public void getExemptionsByConcept_shouldReturnExemptionsForSpecificConcept() { + Concept concept = conceptService.getConcept(100); + assertNotNull(concept); + + List exemptions = dao.getExemptionsByConcept(concept, null, false); + + assertNotNull(exemptions); + assertEquals(1, exemptions.size()); + assertEquals("Service Exemption 1", exemptions.get(0).getName()); + } + + /** + * @see BillExemptionDAO#getExemptionsByConcept(Concept, ExemptionType, boolean) + */ + @Test + public void getExemptionsByConcept_shouldReturnExemptionsFilteredByExemptionType() { + Concept concept = conceptService.getConcept(100); + assertNotNull(concept); + + List serviceExemptions = dao.getExemptionsByConcept(concept, ExemptionType.SERVICE, false); + + assertNotNull(serviceExemptions); + assertEquals(1, serviceExemptions.size()); + assertEquals(ExemptionType.SERVICE, serviceExemptions.get(0).getExemptionType()); + } + + /** + * @see BillExemptionDAO#getExemptionsByConcept(Concept, ExemptionType, boolean) + */ + @Test + public void getExemptionsByConcept_shouldNotReturnRetiredExemptionsWhenIncludeRetiredIsFalse() { + Concept concept = conceptService.getConcept(103); + assertNotNull(concept); + + List exemptions = dao.getExemptionsByConcept(concept, null, false); + + assertNotNull(exemptions); + assertTrue(exemptions.isEmpty() || exemptions.stream().noneMatch(BaseOpenmrsMetadata::getRetired)); + } + + /** + * @see BillExemptionDAO#getExemptionsByConcept(Concept, ExemptionType, boolean) + */ + @Test + public void getExemptionsByConcept_shouldReturnOnlyRetiredExemptionsWhenIncludeRetiredIsTrue() { + Concept concept = conceptService.getConcept(103); + assertNotNull(concept); + + List exemptions = dao.getExemptionsByConcept(concept, null, true); + + assertNotNull(exemptions); + assertFalse(exemptions.isEmpty()); + assertTrue(exemptions.stream().allMatch(BaseOpenmrsMetadata::getRetired)); + assertEquals("Retired Service Exemption", exemptions.get(0).getName()); + } + + /** + * @see BillExemptionDAO#getExemptionsByItemType(ExemptionType, boolean) + */ + @Test + public void getExemptionsByItemType_shouldReturnAllServiceExemptions() { + List serviceExemptions = dao.getExemptionsByItemType(ExemptionType.SERVICE, false); + + assertNotNull(serviceExemptions); + assertTrue(!serviceExemptions.isEmpty()); + assertTrue( + serviceExemptions.stream().allMatch(e -> e.getExemptionType() == ExemptionType.SERVICE && !e.getRetired())); + } + + /** + * @see BillExemptionDAO#getExemptionsByItemType(ExemptionType, boolean) + */ + @Test + public void getExemptionsByItemType_shouldReturnAllCommodityExemptions() { + List commodityExemptions = dao.getExemptionsByItemType(ExemptionType.COMMODITY, false); + + assertNotNull(commodityExemptions); + assertEquals(1, commodityExemptions.size()); + assertEquals(ExemptionType.COMMODITY, commodityExemptions.get(0).getExemptionType()); + assertEquals("Commodity Exemption 1", commodityExemptions.get(0).getName()); + } + + /** + * @see BillExemptionDAO#getExemptionsByItemType(ExemptionType, boolean) + */ + @Test + public void getExemptionsByItemType_shouldReturnBothTypeExemptions() { + List bothTypeExemptions = dao.getExemptionsByItemType(ExemptionType.BOTH, false); + + assertNotNull(bothTypeExemptions); + assertEquals(1, bothTypeExemptions.size()); + assertEquals(ExemptionType.BOTH, bothTypeExemptions.get(0).getExemptionType()); + assertEquals("Both Type Exemption", bothTypeExemptions.get(0).getName()); + } + + /** + * @see BillExemptionDAO#getExemptionsByItemType(ExemptionType, boolean) + */ + @Test + public void getExemptionsByItemType_shouldReturnAllExemptionsIncludingRetiredWhenIncludeRetiredIsTrue() { + List allExemptions = dao.getExemptionsByItemType(ExemptionType.SERVICE, true); + + assertNotNull(allExemptions); + assertFalse(allExemptions.isEmpty()); + assertTrue(allExemptions.size() >= 2); + assertTrue(allExemptions.stream().anyMatch(BaseOpenmrsMetadata::getRetired)); + assertTrue(allExemptions.stream().anyMatch(e -> !e.getRetired())); + } + + /** + * @see BillExemptionDAO#save(BillExemption) + */ + @Test + public void save_shouldSaveNewBillingExemption() { + Concept concept = conceptService.getConcept(100); + assertNotNull(concept); + + BillExemption newExemption = new BillExemption(); + newExemption.setName("New Test Exemption"); + newExemption.setDescription("Test exemption created by test"); + newExemption.setConcept(concept); + newExemption.setExemptionType(ExemptionType.SERVICE); + newExemption.setCreator(Context.getAuthenticatedUser()); + newExemption.setDateCreated(new Date()); + + BillExemption saved = dao.save(newExemption); + + assertNotNull(saved); + assertNotNull(saved.getExemptionId()); + assertEquals("New Test Exemption", saved.getName()); + assertEquals(ExemptionType.SERVICE, saved.getExemptionType()); + assertEquals(concept.getId(), saved.getConcept().getId()); + } + + /** + * @see BillExemptionDAO#save(BillExemption) + */ + @Test + public void save_shouldUpdateExistingBillingExemption() { + BillExemption exemption = dao.getBillingExemptionById(1); + assertNotNull(exemption); + + String originalName = exemption.getName(); + String newName = "Updated Service Exemption"; + exemption.setName(newName); + + BillExemption updated = dao.save(exemption); + + assertNotNull(updated); + assertEquals(1, updated.getExemptionId()); + assertEquals(newName, updated.getName()); + assertTrue(!originalName.equals(updated.getName())); + } + + /** + * @see BillExemptionDAO#getBillingExemptionById(Integer) + */ + @Test + public void getBillingExemptionById_shouldLoadExemptionWithRules() { + BillExemption exemption = dao.getBillingExemptionById(1); + + assertNotNull(exemption); + assertNotNull(exemption.getRules()); + assertFalse(exemption.getRules().isEmpty()); + + BillExemptionRule rule = exemption.getRules().get(0); + assertNotNull(rule); + assertEquals("patientAge < 5", rule.getScript()); + } + + /** + * @see BillExemptionDAO#getExemptionsByConcept(Concept, ExemptionType, boolean) + */ + @Test + public void getExemptionsByConcept_shouldReturnEmptyListWhenNoMatchingExemptions() { + Concept concept = conceptService.getConcept(100); + assertNotNull(concept); + + List exemptions = dao.getExemptionsByConcept(concept, ExemptionType.COMMODITY, false); + + assertNotNull(exemptions); + assertTrue(exemptions.isEmpty()); + } + + /** + * @see BillExemptionDAO#getExemptionsByItemType(ExemptionType, boolean) + */ + @Test + public void getExemptionsByItemType_shouldReturnAllExemptionsWhenItemTypeIsNull() { + List allExemptions = dao.getExemptionsByItemType(null, false); + + assertNotNull(allExemptions); + assertTrue(allExemptions.size() >= 3); + assertTrue(allExemptions.stream().noneMatch(BaseOpenmrsMetadata::getRetired)); + } +} diff --git a/api/src/test/java/org/openmrs/module/billing/api/evaluator/ExemptionRuleEngineTest.java b/api/src/test/java/org/openmrs/module/billing/api/evaluator/ExemptionRuleEngineTest.java new file mode 100644 index 00000000..d98626db --- /dev/null +++ b/api/src/test/java/org/openmrs/module/billing/api/evaluator/ExemptionRuleEngineTest.java @@ -0,0 +1,289 @@ +/* + * The contents of this file are subject to the OpenMRS Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://license.openmrs.org + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the + * License for the specific language governing rights and limitations + * under the License. + * + * Copyright (C) OpenMRS, LLC. All Rights Reserved. + */ + +package org.openmrs.module.billing.api.evaluator; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openmrs.Concept; +import org.openmrs.Order; +import org.openmrs.Patient; +import org.openmrs.api.ConceptService; +import org.openmrs.api.OrderService; +import org.openmrs.api.PatientService; +import org.openmrs.api.context.Context; +import org.openmrs.module.billing.TestConstants; +import org.openmrs.module.billing.api.BillExemptionService; +import org.openmrs.module.billing.api.evaluator.impl.JSExemptionEvaluator; +import org.openmrs.module.billing.api.model.BillExemption; +import org.openmrs.module.billing.api.model.BillExemptionRule; +import org.openmrs.module.billing.api.model.ExemptionType; +import org.openmrs.test.jupiter.BaseModuleContextSensitiveTest; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ExemptionRuleEngineTest extends BaseModuleContextSensitiveTest { + + private ExemptionRuleEngine ruleEngine; + + private BillExemptionService billExemptionService; + + private ConceptService conceptService; + + private PatientService patientService; + + private OrderService orderService; + + @BeforeEach + public void setup() { + List evaluators = new ArrayList<>(); + evaluators.add(new JSExemptionEvaluator()); + ruleEngine = new ExemptionRuleEngine(evaluators); + + billExemptionService = Context.getService(BillExemptionService.class); + conceptService = Context.getConceptService(); + patientService = Context.getPatientService(); + orderService = Context.getOrderService(); + + executeDataSet(TestConstants.CORE_DATASET2); + executeDataSet(TestConstants.BASE_DATASET_DIR + "BillExemptionTest.xml"); + } + + /** + * @see ExemptionRuleEngine#evaluateRule(BillExemptionRule, Map) + */ + @Test + public void evaluateRule_shouldEvaluateSimpleRule() { + BillExemptionRule rule = new BillExemptionRule(); + rule.setScriptType(ScriptType.JAVASCRIPT); + rule.setScript("age < 18"); + + Map variables = new HashMap<>(); + variables.put("age", 10); + + boolean result = ruleEngine.evaluateRule(rule, variables); + + assertTrue(result); + } + + /** + * @see ExemptionRuleEngine#evaluateRule(BillExemptionRule, Map) + */ + @Test + public void evaluateRule_shouldReturnFalseWhenRuleFails() { + BillExemptionRule rule = new BillExemptionRule(); + rule.setScriptType(ScriptType.JAVASCRIPT); + rule.setScript("age < 18"); + + Map variables = new HashMap<>(); + variables.put("age", 25); + + boolean result = ruleEngine.evaluateRule(rule, variables); + + assertFalse(result); + } + + /** + * @see ExemptionRuleEngine#isExemptionApplicable(BillExemption, Map) + */ + @Test + public void isExemptionApplicable_shouldReturnTrueWhenAnyRuleMatches() { + BillExemption exemption = billExemptionService.getBillingExemptionById(1); + assertNotNull(exemption); + assertNotNull(exemption.getRules()); + assertFalse(exemption.getRules().isEmpty()); + + Map variables = new HashMap<>(); + variables.put("patientAge", 3); + + boolean result = ruleEngine.isExemptionApplicable(exemption, variables); + + assertTrue(result); + } + + /** + * @see ExemptionRuleEngine#isExemptionApplicable(BillExemption, Map) + */ + @Test + public void isExemptionApplicable_shouldReturnFalseWhenNoRuleMatches() { + BillExemption exemption = billExemptionService.getBillingExemptionById(1); + assertNotNull(exemption); + + Map variables = new HashMap<>(); + variables.put("patientAge", 25); + + boolean result = ruleEngine.isExemptionApplicable(exemption, variables); + + assertFalse(result); + } + + /** + * Integration test mimicking actual order exemption check + */ + @Test + public void checkIfOrderIsExempted_shouldExemptChildrenUnderFive() { + Patient patient = patientService.getPatient(2); + assertNotNull(patient); + + Concept concept = conceptService.getConcept(100); + assertNotNull(concept); + + Order order = new Order(); + order.setPatient(patient); + order.setConcept(concept); + + List exemptions = billExemptionService.getExemptionsByConcept(concept, ExemptionType.SERVICE, false); + + assertNotNull(exemptions); + assertFalse(exemptions.isEmpty()); + + Map variables = buildVariablesMapForOrder(order); + + boolean isExempted = false; + for (BillExemption exemption : exemptions) { + if (ruleEngine.isExemptionApplicable(exemption, variables)) { + isExempted = true; + break; + } + } + + assertTrue(isExempted); + } + + /** + * Integration test with active programs + */ + @Test + public void checkIfOrderIsExempted_shouldCheckActivePrograms() { + Patient patient = patientService.getPatient(2); + assertNotNull(patient); + + Concept concept = conceptService.getConcept(100); + assertNotNull(concept); + + Order order = new Order(); + order.setPatient(patient); + order.setConcept(concept); + + Set activePrograms = new HashSet<>(); + activePrograms.add("HIV Program"); + activePrograms.add("TB Program"); + + Map variables = new HashMap<>(); + variables.put("order", order); + variables.put("patient", patient); + variables.put("patientAge", 4); + variables.put("activePrograms", activePrograms); + + List exemptions = billExemptionService.getExemptionsByConcept(concept, ExemptionType.SERVICE, false); + + boolean isExempted = false; + for (BillExemption exemption : exemptions) { + if (ruleEngine.isExemptionApplicable(exemption, variables)) { + isExempted = true; + break; + } + } + + assertTrue(isExempted); + } + + /** + * Test with elderly patient (>= 65 years) + */ + @Test + public void checkIfOrderIsExempted_shouldExemptElderlyPatients() { + Patient patient = patientService.getPatient(2); + assertNotNull(patient); + + Concept commodityConcept = conceptService.getConcept(102); + assertNotNull(commodityConcept); + + Order order = new Order(); + order.setPatient(patient); + order.setConcept(commodityConcept); + + List exemptions = billExemptionService.getExemptionsByConcept(commodityConcept, + ExemptionType.COMMODITY, false); + + assertNotNull(exemptions); + assertFalse(exemptions.isEmpty()); + + Map variables = new HashMap<>(); + variables.put("order", order); + variables.put("patientAge", 70); + + boolean isExempted = false; + for (BillExemption exemption : exemptions) { + if (ruleEngine.isExemptionApplicable(exemption, variables)) { + isExempted = true; + break; + } + } + + assertTrue(isExempted); + } + + /** + * Test that non-exempted orders return false + */ + @Test + public void checkIfOrderIsExempted_shouldNotExemptNonQualifyingOrders() { + Patient patient = patientService.getPatient(2); + assertNotNull(patient); + + Concept concept = conceptService.getConcept(100); + assertNotNull(concept); + + Order order = new Order(); + order.setPatient(patient); + order.setConcept(concept); + + Map variables = new HashMap<>(); + variables.put("order", order); + variables.put("patientAge", 30); + + List exemptions = billExemptionService.getExemptionsByConcept(concept, ExemptionType.SERVICE, false); + + boolean isExempted = false; + for (BillExemption exemption : exemptions) { + if (ruleEngine.isExemptionApplicable(exemption, variables)) { + isExempted = true; + break; + } + } + + assertFalse(isExempted); + } + + private Map buildVariablesMapForOrder(Order order) { + Map variables = new HashMap<>(); + variables.put("order", order); + variables.put("patientAge", 4); + + Set activePrograms = new HashSet<>(); + variables.put("activePrograms", activePrograms); + + return variables; + } +} diff --git a/api/src/test/java/org/openmrs/module/billing/api/evaluator/impl/JSExemptionEvaluatorTest.java b/api/src/test/java/org/openmrs/module/billing/api/evaluator/impl/JSExemptionEvaluatorTest.java new file mode 100644 index 00000000..0f1d0ca5 --- /dev/null +++ b/api/src/test/java/org/openmrs/module/billing/api/evaluator/impl/JSExemptionEvaluatorTest.java @@ -0,0 +1,119 @@ +/* + * The contents of this file are subject to the OpenMRS Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://license.openmrs.org + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the + * License for the specific language governing rights and limitations + * under the License. + * + * Copyright (C) OpenMRS, LLC. All Rights Reserved. + */ + +package org.openmrs.module.billing.api.evaluator.impl; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openmrs.module.billing.api.evaluator.ScriptType; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class JSExemptionEvaluatorTest { + + private JSExemptionEvaluator evaluator; + + @BeforeEach + public void setup() { + evaluator = new JSExemptionEvaluator(); + } + + /** + * @see JSExemptionEvaluator#getSupportedType() + */ + @Test + public void getSupportedType_shouldReturnJavaScript() { + assertEquals(ScriptType.JAVASCRIPT, evaluator.getSupportedType()); + } + + /** + * @see JSExemptionEvaluator#evaluate(String, Map) + */ + @Test + public void evaluate_shouldReturnTrueForTrueScript() { + boolean result = evaluator.evaluate("true", null); + assertTrue(result); + } + + /** + * @see JSExemptionEvaluator#evaluate(String, Map) + */ + @Test + public void evaluate_shouldReturnFalseForFalseScript() { + boolean result = evaluator.evaluate("false", null); + assertFalse(result); + } + + /** + * @see JSExemptionEvaluator#evaluate(String, Map) + */ + @Test + public void evaluate_shouldEvaluateSimpleComparison() { + Map variables = new HashMap<>(); + variables.put("age", 10); + + boolean result = evaluator.evaluate("age < 18", variables); + assertTrue(result); + } + + /** + * @see JSExemptionEvaluator#evaluate(String, Map) + */ + @Test + public void evaluate_shouldReturnFalseWhenComparisonFails() { + Map variables = new HashMap<>(); + variables.put("age", 25); + + boolean result = evaluator.evaluate("age < 18", variables); + assertFalse(result); + } + + /** + * @see JSExemptionEvaluator#evaluate(String, Map) + */ + @Test + public void evaluate_shouldHandleComplexExpressions() { + Map variables = new HashMap<>(); + variables.put("age", 5); + variables.put("hasInsurance", false); + + boolean result = evaluator.evaluate("age < 18 && !hasInsurance", variables); + assertTrue(result); + } + + /** + * @see JSExemptionEvaluator#evaluate(String, Map) + */ + @Test + public void evaluate_shouldReturnFalseForNullResult() { + boolean result = evaluator.evaluate("null", null); + assertFalse(result); + } + + /** + * @see JSExemptionEvaluator#evaluate(String, Map) + */ + @Test + public void evaluate_shouldThrowExceptionForInvalidScript() { + assertThrows(RuntimeException.class, () -> { + evaluator.evaluate("invalid javascript +++", null); + }); + } +} diff --git a/api/src/test/java/org/openmrs/module/billing/api/impl/BillExemptionServiceImplTest.java b/api/src/test/java/org/openmrs/module/billing/api/impl/BillExemptionServiceImplTest.java new file mode 100644 index 00000000..8a004e5f --- /dev/null +++ b/api/src/test/java/org/openmrs/module/billing/api/impl/BillExemptionServiceImplTest.java @@ -0,0 +1,137 @@ +/* + * The contents of this file are subject to the OpenMRS Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://license.openmrs.org + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the + * License for the specific language governing rights and limitations + * under the License. + * + * Copyright (C) OpenMRS, LLC. All Rights Reserved. + */ + +package org.openmrs.module.billing.api.impl; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openmrs.Concept; +import org.openmrs.api.ConceptService; +import org.openmrs.api.context.Context; +import org.openmrs.module.billing.TestConstants; +import org.openmrs.module.billing.api.BillExemptionService; +import org.openmrs.module.billing.api.model.BillExemption; +import org.openmrs.module.billing.api.model.ExemptionType; +import org.openmrs.test.jupiter.BaseModuleContextSensitiveTest; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class BillExemptionServiceImplTest extends BaseModuleContextSensitiveTest { + + private static final String EXEMPTION_UUID_1 = "3386610d-d272-43a9-9083-6c2a5272ade9"; + + private BillExemptionService service; + + private ConceptService conceptService; + + @BeforeEach + public void setup() { + service = Context.getService(BillExemptionService.class); + conceptService = Context.getConceptService(); + executeDataSet(TestConstants.CORE_DATASET2); + executeDataSet(TestConstants.BASE_DATASET_DIR + "BillExemptionTest.xml"); + } + + /** + * @see BillExemptionService#getBillingExemptionById(Integer) + */ + @Test + public void getBillingExemptionById_shouldReturnExemptionWithSpecifiedId() { + BillExemption exemption = service.getBillingExemptionById(1); + + assertNotNull(exemption); + assertEquals(1, exemption.getExemptionId()); + assertEquals("Service Exemption 1", exemption.getName()); + } + + /** + * @see BillExemptionService#getBillingExemptionById(Integer) + */ + @Test + public void getBillingExemptionById_shouldReturnNullForInvalidId() { + BillExemption exemption = service.getBillingExemptionById(999); + + assertNull(exemption); + } + + /** + * @see BillExemptionService#getBillingExemptionByUuid(String) + */ + @Test + public void getBillingExemptionByUuid_shouldReturnExemptionWithSpecifiedUuid() { + BillExemption exemption = service.getBillingExemptionByUuid(EXEMPTION_UUID_1); + + assertNotNull(exemption); + assertEquals(EXEMPTION_UUID_1, exemption.getUuid()); + assertEquals("Service Exemption 1", exemption.getName()); + } + + /** + * @see BillExemptionService#getExemptionsByConcept(Concept, ExemptionType, boolean) + */ + @Test + public void getExemptionsByConcept_shouldReturnExemptionsForConcept() { + Concept concept = conceptService.getConcept(100); + + List exemptions = service.getExemptionsByConcept(concept, null, false); + + assertNotNull(exemptions); + assertFalse(exemptions.isEmpty()); + assertEquals("Service Exemption 1", exemptions.get(0).getName()); + } + + /** + * @see BillExemptionService#getExemptionsByItemType(ExemptionType, boolean) + */ + @Test + public void getExemptionsByItemType_shouldReturnServiceExemptions() { + List serviceExemptions = service.getExemptionsByItemType(ExemptionType.SERVICE, false); + + assertNotNull(serviceExemptions); + assertFalse(serviceExemptions.isEmpty()); + assertEquals(ExemptionType.SERVICE, serviceExemptions.get(0).getExemptionType()); + } + + /** + * @see BillExemptionService#getExemptionsByItemType(ExemptionType, boolean) + */ + @Test + public void getExemptionsByItemType_shouldReturnCommodityExemptions() { + List commodityExemptions = service.getExemptionsByItemType(ExemptionType.COMMODITY, false); + + assertNotNull(commodityExemptions); + assertEquals(1, commodityExemptions.size()); + assertEquals(ExemptionType.COMMODITY, commodityExemptions.get(0).getExemptionType()); + } + + /** + * @see BillExemptionService#save(BillExemption) + */ + @Test + public void save_shouldUpdateExemption() { + BillExemption exemption = service.getBillingExemptionById(1); + assertNotNull(exemption); + + exemption.setName("Updated Name"); + BillExemption updated = service.save(exemption); + + assertNotNull(updated); + assertEquals("Updated Name", updated.getName()); + } +} diff --git a/api/src/test/java/org/openmrs/module/billing/api/model/BillTest.java b/api/src/test/java/org/openmrs/module/billing/api/model/BillTest.java new file mode 100644 index 00000000..a043cd2a --- /dev/null +++ b/api/src/test/java/org/openmrs/module/billing/api/model/BillTest.java @@ -0,0 +1,195 @@ +package org.openmrs.module.billing.api.model; + +import static org.junit.Assert.assertEquals; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.HashSet; + +import org.junit.Test; + +/** + * Test for verifying Bill model methods, particularly getTotalPayments() + */ +public class BillTest { + + @Test + public void getTotalPayments_shouldExcludeVoidedPaymentsFromTotal() { + Bill bill = new Bill(); + bill.setPayments(new HashSet<>()); + + Payment validPayment1 = new Payment(); + validPayment1.setAmountTendered(BigDecimal.valueOf(50)); + validPayment1.setVoided(false); + bill.getPayments().add(validPayment1); + + Payment validPayment2 = new Payment(); + validPayment2.setAmountTendered(BigDecimal.valueOf(30)); + validPayment2.setVoided(false); + bill.getPayments().add(validPayment2); + + Payment voidedPayment1 = new Payment(); + voidedPayment1.setAmountTendered(BigDecimal.valueOf(20)); + voidedPayment1.setVoided(true); + bill.getPayments().add(voidedPayment1); + + Payment voidedPayment2 = new Payment(); + voidedPayment2.setAmountTendered(BigDecimal.valueOf(40)); + voidedPayment2.setVoided(true); + bill.getPayments().add(voidedPayment2); + + assertEquals(BigDecimal.valueOf(80), bill.getTotalPayments()); + } + + @Test + public void getTotalPayments_shouldReturnZeroWhenAllPaymentsAreVoided() { + Bill bill = new Bill(); + bill.setPayments(new HashSet<>()); + + Payment voidedPayment = new Payment(); + voidedPayment.setAmountTendered(BigDecimal.valueOf(100)); + voidedPayment.setVoided(true); + bill.getPayments().add(voidedPayment); + + assertEquals(BigDecimal.ZERO, bill.getTotalPayments()); + } + + @Test + public void getTotal_shouldExcludeVoidedLineItemsFromTotal() { + Bill bill = new Bill(); + bill.setLineItems(new ArrayList<>()); + + BillLineItem lineItem1 = new BillLineItem(); + lineItem1.setPrice(BigDecimal.valueOf(100)); + lineItem1.setQuantity(2); + lineItem1.setVoided(false); + bill.getLineItems().add(lineItem1); + + BillLineItem lineItem2 = new BillLineItem(); + lineItem2.setPrice(BigDecimal.valueOf(50)); + lineItem2.setQuantity(1); + lineItem2.setVoided(false); + bill.getLineItems().add(lineItem2); + + BillLineItem voidedLineItem1 = new BillLineItem(); + voidedLineItem1.setPrice(BigDecimal.valueOf(75)); + voidedLineItem1.setQuantity(3); + voidedLineItem1.setVoided(true); + bill.getLineItems().add(voidedLineItem1); + + BillLineItem voidedLineItem2 = new BillLineItem(); + voidedLineItem2.setPrice(BigDecimal.valueOf(30)); + voidedLineItem2.setQuantity(2); + voidedLineItem2.setVoided(true); + bill.getLineItems().add(voidedLineItem2); + + assertEquals(BigDecimal.valueOf(250), bill.getTotal()); + } + + @Test + public void getTotal_shouldReturnZeroWhenAllLineItemsAreVoided() { + Bill bill = new Bill(); + bill.setLineItems(new ArrayList<>()); + + BillLineItem voidedLineItem = new BillLineItem(); + voidedLineItem.setPrice(BigDecimal.valueOf(100)); + voidedLineItem.setQuantity(5); + voidedLineItem.setVoided(true); + bill.getLineItems().add(voidedLineItem); + + assertEquals(BigDecimal.ZERO, bill.getTotal()); + } + + @Test + public void synchronizeBillStatus_shouldUpdateStatusToPaidWhenFullyPaid() { + Bill bill = new Bill(); + bill.setLineItems(new ArrayList<>()); + bill.setPayments(new HashSet<>()); + + BillLineItem lineItem = new BillLineItem(); + lineItem.setPrice(BigDecimal.valueOf(100)); + lineItem.setQuantity(1); + lineItem.setVoided(false); + bill.getLineItems().add(lineItem); + + Payment payment = new Payment(); + payment.setAmountTendered(BigDecimal.valueOf(100)); + payment.setVoided(false); + bill.getPayments().add(payment); + + bill.synchronizeBillStatus(); + + assertEquals(BillStatus.PAID, bill.getStatus()); + } + + @Test + public void synchronizeBillStatus_shouldUpdateStatusToPostedWhenPartiallyPaid() { + Bill bill = new Bill(); + bill.setLineItems(new ArrayList<>()); + bill.setPayments(new HashSet<>()); + + BillLineItem lineItem = new BillLineItem(); + lineItem.setPrice(BigDecimal.valueOf(100)); + lineItem.setQuantity(1); + lineItem.setVoided(false); + bill.getLineItems().add(lineItem); + + Payment payment = new Payment(); + payment.setAmountTendered(BigDecimal.valueOf(50)); + payment.setVoided(false); + bill.getPayments().add(payment); + + bill.synchronizeBillStatus(); + + assertEquals(BillStatus.POSTED, bill.getStatus()); + } + + @Test + public void synchronizeBillStatus_shouldUpdateStatusToPaidAfterVoidingLineItems() { + Bill bill = new Bill(); + bill.setLineItems(new ArrayList<>()); + bill.setPayments(new HashSet<>()); + + BillLineItem lineItem1 = new BillLineItem(); + lineItem1.setPrice(BigDecimal.valueOf(100)); + lineItem1.setQuantity(1); + lineItem1.setVoided(false); + bill.getLineItems().add(lineItem1); + + BillLineItem lineItem2 = new BillLineItem(); + lineItem2.setPrice(BigDecimal.valueOf(50)); + lineItem2.setQuantity(1); + lineItem2.setVoided(false); + bill.getLineItems().add(lineItem2); + + Payment payment = new Payment(); + payment.setAmountTendered(BigDecimal.valueOf(100)); + payment.setVoided(false); + bill.getPayments().add(payment); + + bill.synchronizeBillStatus(); + assertEquals(BillStatus.POSTED, bill.getStatus()); + + lineItem2.setVoided(true); + + bill.synchronizeBillStatus(); + assertEquals(BillStatus.PAID, bill.getStatus()); + } + + @Test + public void setLineItems_shouldAllowSettingLineItemsOnNewBill() { + Bill bill = new Bill(); + bill.setStatus(BillStatus.PENDING); + + ArrayList lineItems = new ArrayList<>(); + BillLineItem lineItem = new BillLineItem(); + lineItem.setPrice(BigDecimal.valueOf(100)); + lineItem.setQuantity(1); + lineItems.add(lineItem); + + // Should not throw exception for new bill (no ID) + bill.setLineItems(lineItems); + assertEquals(1, bill.getLineItems().size()); + } + +} diff --git a/api/src/test/java/org/openmrs/module/billing/base/entity/IObjectDataServiceTest.java b/api/src/test/java/org/openmrs/module/billing/base/entity/IObjectDataServiceTest.java index f4060a55..d9697201 100644 --- a/api/src/test/java/org/openmrs/module/billing/base/entity/IObjectDataServiceTest.java +++ b/api/src/test/java/org/openmrs/module/billing/base/entity/IObjectDataServiceTest.java @@ -88,8 +88,8 @@ public void before() throws Exception { * @see org.openmrs.module.openhmis.commons.api.entity.IObjectDataService#save(OpenmrsObject) */ @Test(expected = NullPointerException.class) - public void save_shouldThrowNullPointerExceptionIfTheObjectIsNull() throws Exception { - service.save(null); + public void save_Bill_shouldThrowNullPointerExceptionIfTheObjectIsNull() throws Exception { + service.saveBill(null); } /** @@ -97,10 +97,10 @@ public void save_shouldThrowNullPointerExceptionIfTheObjectIsNull() throws Excep * @see org.openmrs.module.openhmis.commons.api.entity.IObjectDataService#save(OpenmrsObject) */ @Test(expected = APIException.class) - public void save_shouldValidateTheObjectBeforeSaving() throws Exception { + public void save_Bill_shouldValidateTheObjectBeforeSaving() throws Exception { E entity = createEntity(false); - service.save(entity); + service.saveBill(entity); } /** @@ -108,10 +108,10 @@ public void save_shouldValidateTheObjectBeforeSaving() throws Exception { * @see org.openmrs.module.openhmis.commons.api.entity.IObjectDataService#save(OpenmrsObject) */ @Test - public void save_shouldReturnSavedObject() throws Exception { + public void save_Bill_shouldReturnSavedObject() throws Exception { E entity = createEntity(true); - E result = service.save(entity); + E result = service.saveBill(entity); Context.flushSession(); Assert.assertNotNull(result); @@ -123,13 +123,13 @@ public void save_shouldReturnSavedObject() throws Exception { * @see org.openmrs.module.openhmis.commons.api.entity.IObjectDataService#save(OpenmrsObject) */ @Test - public void save_shouldUpdateTheObjectSuccessfully() throws Exception { + public void save_Bill_shouldUpdateTheObjectSuccessfully() throws Exception { E entity = service.getById(0); Assert.assertNotNull(entity); updateEntityFields(entity); - service.save(entity); + service.saveBill(entity); Context.flushSession(); E updatedEntity = service.getById(entity.getId()); @@ -141,10 +141,10 @@ public void save_shouldUpdateTheObjectSuccessfully() throws Exception { * @see org.openmrs.module.openhmis.commons.api.entity.IObjectDataService#save(OpenmrsObject) */ @Test - public void save_shouldCreateTheObjectSuccessfully() throws Exception { + public void save_Bill_shouldCreateTheObjectSuccessfully() throws Exception { E entity = createEntity(true); - entity = service.save(entity); + entity = service.saveBill(entity); Context.flushSession(); E result = service.getById(entity.getId()); @@ -168,7 +168,7 @@ public void purge_shouldThrowNullPointerExceptionIfTheObjectIsNull() throws Exce public void purge_shouldDeleteTheSpecifiedObject() throws Exception { E entity = createEntity(true); - service.save(entity); + service.saveBill(entity); Context.flushSession(); E result = service.getById(entity.getId()); diff --git a/api/src/test/java/org/openmrs/module/billing/db/HibernateBillDAOImplTest.java b/api/src/test/java/org/openmrs/module/billing/db/HibernateBillDAOImplTest.java new file mode 100644 index 00000000..7f136782 --- /dev/null +++ b/api/src/test/java/org/openmrs/module/billing/db/HibernateBillDAOImplTest.java @@ -0,0 +1,243 @@ +/* + * The contents of this file are subject to the OpenMRS Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://license.openmrs.org + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the + * License for the specific language governing rights and limitations + * under the License. + * + * Copyright (C) OpenMRS, LLC. All Rights Reserved. + */ +package org.openmrs.module.billing.db; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openmrs.Patient; +import org.openmrs.api.PatientService; +import org.openmrs.api.ProviderService; +import org.openmrs.api.context.Context; +import org.openmrs.module.billing.TestConstants; +import org.openmrs.module.billing.api.ICashPointService; +import org.openmrs.module.billing.api.base.PagingInfo; +import org.openmrs.module.billing.api.db.BillDAO; +import org.openmrs.module.billing.api.model.Bill; +import org.openmrs.module.billing.api.model.BillStatus; +import org.openmrs.module.billing.api.search.BillSearch; +import org.openmrs.test.jupiter.BaseModuleContextSensitiveTest; + +public class HibernateBillDAOImplTest extends BaseModuleContextSensitiveTest { + + private BillDAO billDAO; + + private PatientService patientService; + + private ProviderService providerService; + + private ICashPointService cashPointService; + + @BeforeEach + public void setup() { + billDAO = Context.getRegisteredComponent("billDAO", BillDAO.class); + patientService = Context.getPatientService(); + providerService = Context.getProviderService(); + cashPointService = Context.getService(ICashPointService.class); + + executeDataSet(TestConstants.CORE_DATASET2); + executeDataSet(TestConstants.BASE_DATASET_DIR + "StockOperationType.xml"); + executeDataSet(TestConstants.BASE_DATASET_DIR + "PaymentModeTest.xml"); + executeDataSet(TestConstants.BASE_DATASET_DIR + "CashPointTest.xml"); + executeDataSet(TestConstants.BASE_DATASET_DIR + "BillTest.xml"); + } + + @Test + public void getBill_shouldReturnBillById() { + Bill bill = billDAO.getBill(0); + assertNotNull(bill); + assertEquals(0, bill.getId()); + } + + @Test + public void getBill_shouldReturnNullIfBillNotFound() { + Bill bill = billDAO.getBill(999); + assertNull(bill); + } + + @Test + public void getBillByUuid_shouldReturnBillByUuid() { + Bill bill = billDAO.getBill(0); + assertNotNull(bill); + String uuid = bill.getUuid(); + + Bill foundBill = billDAO.getBillByUuid(uuid); + assertNotNull(foundBill); + assertEquals(uuid, foundBill.getUuid()); + assertEquals(0, foundBill.getId()); + } + + @Test + public void getBillByUuid_shouldReturnNullIfUuidNotFound() { + Bill bill = billDAO.getBillByUuid("nonexistent-uuid"); + assertNull(bill); + } + + @Test + public void saveBill_shouldCreateNewBill() { + Patient patient = patientService.getPatient(1); + assertNotNull(patient); + + Bill newBill = new Bill(); + newBill.setCashier(providerService.getProvider(0)); + newBill.setPatient(patient); + newBill.setCashPoint(cashPointService.getById(0)); + newBill.setReceiptNumber("TEST-" + UUID.randomUUID()); + newBill.setStatus(BillStatus.PENDING); + + Bill savedBill = billDAO.saveBill(newBill); + Context.flushSession(); + + assertNotNull(savedBill); + assertNotNull(savedBill.getId()); + assertEquals(BillStatus.PENDING, savedBill.getStatus()); + } + + @Test + public void saveBill_shouldUpdateExistingBill() { + Bill existingBill = billDAO.getBill(2); + assertNotNull(existingBill); + assertEquals(BillStatus.PENDING, existingBill.getStatus()); + + String newReceiptNumber = "UPDATED-" + UUID.randomUUID(); + existingBill.setReceiptNumber(newReceiptNumber); + + billDAO.saveBill(existingBill); + Context.flushSession(); + Context.clearSession(); + + Bill updatedBill = billDAO.getBill(2); + assertEquals(newReceiptNumber, updatedBill.getReceiptNumber()); + } + + @Test + public void getBillByReceiptNumber_shouldReturnBillWithMatchingReceiptNumber() { + Bill bill = billDAO.getBillByReceiptNumber("test 1 receipt number"); + assertNotNull(bill); + assertEquals("test 1 receipt number", bill.getReceiptNumber()); + } + + @Test + public void getBillByReceiptNumber_shouldReturnNullIfReceiptNumberNotFound() { + Bill bill = billDAO.getBillByReceiptNumber("nonexistent receipt number"); + assertNull(bill); + } + + @Test + public void getBillsByPatientId_shouldReturnBillsForPatient() { + List bills = billDAO.getBillsByPatientUuid("5631b434-78aa-102b-91a0-001e378eb67e", null); + assertNotNull(bills); + assertFalse(bills.isEmpty()); + assertEquals(1, bills.size()); + } + + @Test + public void getBillsByPatientId_shouldReturnEmptyListWhenPatientHasNoBills() { + List bills = billDAO.getBillsByPatientUuid("abc", null); + assertNotNull(bills); + assertTrue(bills.isEmpty()); + } + + @Test + public void getBillsByPatientId_shouldApplyPagingCorrectly() { + PagingInfo pagingInfo = new PagingInfo(1, 5); + List bills = billDAO.getBillsByPatientUuid("5631b434-78aa-102b-91a0-001e378eb67e", pagingInfo); + + assertNotNull(bills); + assertTrue(bills.size() <= 5); + } + + @Test + public void getBills_shouldReturnAllBillsWhenSearchIsEmpty() { + BillSearch billSearch = new BillSearch(); + List bills = billDAO.getBills(billSearch, null); + + assertNotNull(bills); + assertFalse(bills.isEmpty()); + } + + @Test + public void getBills_shouldFilterByPatientUuid() { + Patient patient = patientService.getPatient(0); + assertNotNull(patient); + + BillSearch billSearch = new BillSearch(); + billSearch.setPatientUuid(patient.getUuid()); + + List bills = billDAO.getBills(billSearch, null); + assertNotNull(bills); + assertFalse(bills.isEmpty()); + + for (Bill bill : bills) { + assertEquals(patient.getUuid(), bill.getPatient().getUuid()); + } + } + + @Test + public void getBills_shouldFilterByCashPointUuid() { + Bill existingBill = billDAO.getBill(0); + assertNotNull(existingBill); + assertNotNull(existingBill.getCashPoint()); + + BillSearch billSearch = new BillSearch(); + billSearch.setCashPointUuid(existingBill.getCashPoint().getUuid()); + + List bills = billDAO.getBills(billSearch, null); + assertNotNull(bills); + assertFalse(bills.isEmpty()); + } + + @Test + public void getBills_shouldExcludeVoidedBillsByDefault() { + BillSearch billSearch = new BillSearch(); + billSearch.setIncludeVoided(false); + + List bills = billDAO.getBills(billSearch, null); + assertNotNull(bills); + + for (Bill bill : bills) { + assertFalse(bill.getVoided()); + } + } + + @Test + public void purgeBill_shouldDeleteBill() { + Patient patient = patientService.getPatient(1); + assertNotNull(patient); + + Bill newBill = new Bill(); + newBill.setCashier(providerService.getProvider(0)); + newBill.setPatient(patient); + newBill.setCashPoint(cashPointService.getById(0)); + newBill.setReceiptNumber("TO-DELETE-" + UUID.randomUUID()); + newBill.setStatus(BillStatus.PENDING); + + Bill savedBill = billDAO.saveBill(newBill); + Context.flushSession(); + + Integer billId = savedBill.getId(); + assertNotNull(billId); + + billDAO.purgeBill(savedBill); + Context.flushSession(); + Context.clearSession(); + + Bill deletedBill = billDAO.getBill(billId); + assertNull(deletedBill); + } +} diff --git a/api/src/test/java/org/openmrs/module/billing/impl/BillServiceImplTest.java b/api/src/test/java/org/openmrs/module/billing/impl/BillServiceImplTest.java index e8e382c1..d8004b58 100644 --- a/api/src/test/java/org/openmrs/module/billing/impl/BillServiceImplTest.java +++ b/api/src/test/java/org/openmrs/module/billing/impl/BillServiceImplTest.java @@ -1,121 +1,387 @@ -///* -// * The contents of this file are subject to the OpenMRS Public License -// * Version 1.1 (the "License"); you may not use this file except in -// * compliance with the License. You may obtain a copy of the License at -// * http://license.openmrs.org -// * -// * Software distributed under the License is distributed on an "AS IS" -// * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the -// * License for the specific language governing rights and limitations -// * under the License. -// * -// * Copyright (C) OpenMRS, LLC. All Rights Reserved. -// */ -//package org.openmrs.module.cashier.api.impl; -// -//import static org.mockito.Mockito.times; -//import static org.mockito.Mockito.verify; -//import static org.powermock.api.mockito.PowerMockito.mock; -//import static org.powermock.api.mockito.PowerMockito.mockStatic; -//import static org.powermock.api.mockito.PowerMockito.when; -// -//import org.junit.*; -//import org.junit.Before; -//import org.junit.BeforeClass; -//import org.junit.Rule; -//import org.junit.Test; -//import org.openmrs.api.APIException; -//import org.openmrs.api.context.Context; -//import org.openmrs.module.cashier.api.IBillService; -//import org.openmrs.module.cashier.api.IBillServiceTest; -//import org.openmrs.module.cashier.api.IReceiptNumberGenerator; -//import org.openmrs.module.cashier.api.ReceiptNumberGeneratorFactory; -//import org.openmrs.module.cashier.api.model.Bill; -//import org.powermock.core.classloader.annotations.PrepareForTest; -//import org.powermock.modules.agent.PowerMockAgent; -//import org.powermock.modules.junit4.rule.PowerMockRule; -// -//@PrepareForTest(ReceiptNumberGeneratorFactory.class) -//public class BillServiceImplTest extends IBillServiceTest { -// @Rule -// public PowerMockRule rule = new PowerMockRule(); -// -// @BeforeClass -// public static void beforeClass() throws Exception { -// PowerMockAgent.initializeIfNeeded(); -// } -// -// IReceiptNumberGenerator receiptNumberGenerator; -// -// @Before -// public void before() throws Exception { -// super.before(); -// -// mockStatic(ReceiptNumberGeneratorFactory.class); -// receiptNumberGenerator = mock(IReceiptNumberGenerator.class); -// -// when(ReceiptNumberGeneratorFactory.getGenerator()) -// .thenReturn(receiptNumberGenerator); -// } -// -// @Override -// protected IBillService createService() { -// return Context.getService(IBillService.class); -// } -// -// /** -// * @verifies Generate a new receipt number if one has not been defined. -// * @see BillServiceImpl#save(Bill) -// */ -// @Test -// public void save_shouldGenerateANewReceiptNumberIfOneHasNotBeenDefined() throws Exception { -// Bill bill = createEntity(true); -// bill.setReceiptNumber(null); -// -// String receiptNumber = "Test Number"; -// when(receiptNumberGenerator.generateNumber(bill)) -// .thenReturn(receiptNumber); -// -// service.save(bill); -// Context.flushSession(); -// -// Bill savedBill = service.getById(bill.getId()); -// Assert.assertEquals(receiptNumber, savedBill.getReceiptNumber()); -// -// verify(receiptNumberGenerator, times(1)).generateNumber(bill); -// } -// -// /** -// * @verifies Not generate a receipt number if one has already been defined. -// * @see BillServiceImpl#save(Bill) -// */ -// @Test -// public void save_shouldNotGenerateAReceiptNumberIfOneHasAlreadyBeenDefined() throws Exception { -// String receiptNumber = "Test Number"; -// Bill bill = createEntity(true); -// bill.setReceiptNumber(receiptNumber); -// -// service.save(bill); -// Context.flushSession(); -// -// Bill savedBill = service.getById(bill.getId()); -// Assert.assertEquals(receiptNumber, savedBill.getReceiptNumber()); -// -// verify(receiptNumberGenerator, times(0)).generateNumber(bill); -// } -// -// /** -// * @verifies Throw APIException if receipt number cannot be generated. -// * @see BillServiceImpl#save(Bill) -// */ -// @Test(expected = APIException.class) -// public void save_shouldThrowAPIExceptionIfReceiptNumberCannotBeGenerated() throws Exception { -// Bill bill = createEntity(true); -// bill.setReceiptNumber(null); -// -// when(receiptNumberGenerator.generateNumber(bill)) -// .thenThrow(new APIException("Test exception")); -// -// service.save(bill); -// } -//} +/* + * The contents of this file are subject to the OpenMRS Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://license.openmrs.org + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the + * License for the specific language governing rights and limitations + * under the License. + * + * Copyright (C) OpenMRS, LLC. All Rights Reserved. + */ + +package org.openmrs.module.billing.impl; + +import static org.junit.jupiter.api.Assertions.*; + +import java.math.BigDecimal; +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openmrs.Patient; +import org.openmrs.api.PatientService; +import org.openmrs.api.ProviderService; +import org.openmrs.api.context.Context; +import org.openmrs.module.billing.TestConstants; +import org.openmrs.module.billing.api.BillService; +import org.openmrs.module.billing.api.ICashPointService; +import org.openmrs.module.billing.api.base.PagingInfo; +import org.openmrs.module.billing.api.model.Bill; +import org.openmrs.module.billing.api.model.BillLineItem; +import org.openmrs.module.billing.api.model.BillStatus; +import org.openmrs.module.billing.api.search.BillSearch; +import org.openmrs.module.stockmanagement.api.model.StockItem; +import org.openmrs.test.jupiter.BaseModuleContextSensitiveTest; + +public class BillServiceImplTest extends BaseModuleContextSensitiveTest { + + private BillService billService; + + private ProviderService providerService; + + private PatientService patientService; + + private ICashPointService cashPointService; + + @BeforeEach + public void setup() { + billService = Context.getService(BillService.class); + providerService = Context.getProviderService(); + patientService = Context.getPatientService(); + cashPointService = Context.getService(ICashPointService.class); + + executeDataSet(TestConstants.CORE_DATASET2); + executeDataSet(TestConstants.BASE_DATASET_DIR + "StockOperationType.xml"); + executeDataSet(TestConstants.BASE_DATASET_DIR + "PaymentModeTest.xml"); + executeDataSet(TestConstants.BASE_DATASET_DIR + "CashPointTest.xml"); + executeDataSet(TestConstants.BASE_DATASET_DIR + "BillTest.xml"); + } + + /** + * @see org.openmrs.module.billing.api.impl.BillServiceImpl#saveBill(Bill) + */ + @Test + public void save_Bill_shouldThrowNullPointerExceptionIfBillIsNull() { + assertThrows(NullPointerException.class, () -> billService.saveBill(null)); + } + + /** + * @see org.openmrs.module.billing.api.impl.BillServiceImpl#getBillByReceiptNumber(String) + */ + @Test + public void getBillByReceiptNumber_shouldReturnBillWithSpecifiedReceiptNumber() { + Bill bill = billService.getBillByReceiptNumber("test 1 receipt number"); + assertNotNull(bill); + assertEquals("test 1 receipt number", bill.getReceiptNumber()); + } + + /** + * @see org.openmrs.module.billing.api.impl.BillServiceImpl#getBillByReceiptNumber(String) + */ + @Test + public void getBillByReceiptNumber_shouldReturnNullIfReceiptNumberNotFound() { + Bill bill = billService.getBillByReceiptNumber("nonexistent receipt number"); + assertNull(bill); + } + + /** + * @see org.openmrs.module.billing.api.impl.BillServiceImpl#getBillsByPatientUuid(String, + * PagingInfo) + */ + @Test + public void getBillsByPatientUuid_shouldReturnBillsForPatient() { + List bills = billService.getBillsByPatientUuid("5631b434-78aa-102b-91a0-001e378eb67e", null); + assertNotNull(bills); + assertFalse(bills.isEmpty()); + assertEquals(1, bills.size()); + } + + /** + * @see org.openmrs.module.billing.api.impl.BillServiceImpl#getBillsByPatientUuid(String, + * PagingInfo) + */ + @Test + public void getBillsByPatientId_shouldReturnEmptyListWhenPatientHasNoBills() { + List bills = billService.getBillsByPatientUuid("abc", null); + assertNotNull(bills); + assertEquals(0, bills.size()); + } + + /** + * @see org.openmrs.module.billing.api.impl.BillServiceImpl#saveBill(Bill) + */ + @Test + public void save_Bill_shouldCreateNewBillWithNewItem() { + Patient patient = patientService.getPatient(1); + assertNotNull(patient); + + Bill templateBill = billService.getBill(0); + assertNotNull(templateBill); + assertFalse(templateBill.getLineItems().isEmpty()); + + Bill newBill = new Bill(); + newBill.setCashier(providerService.getProvider(0)); + newBill.setPatient(patient); + newBill.setCashPoint(cashPointService.getById(0)); + newBill.setReceiptNumber("TEST-" + UUID.randomUUID()); + newBill.setStatus(BillStatus.PENDING); + + BillLineItem existingItem = templateBill.getLineItems().get(0); + StockItem stockItem = existingItem.getItem(); + + BillLineItem lineItem = newBill.addLineItem(stockItem, BigDecimal.valueOf(150), "New price", 2); + lineItem.setPaymentStatus(BillStatus.PENDING); + lineItem.setUuid(UUID.randomUUID().toString()); + + Bill savedBill = billService.saveBill(newBill); + Context.flushSession(); + + assertNotNull(savedBill); + assertNotNull(savedBill.getId()); + assertEquals(BillStatus.PENDING, savedBill.getStatus()); + assertEquals(1, savedBill.getLineItems().size()); + assertEquals(BigDecimal.valueOf(300), savedBill.getTotal()); + + Bill retrievedBill = billService.getBill(savedBill.getId()); + assertNotNull(retrievedBill); + assertEquals(patient.getId(), retrievedBill.getPatient().getId()); + } + + /** + * @see org.openmrs.module.billing.api.impl.BillServiceImpl#saveBill(Bill) + */ + @Test + public void save_Bill_shouldUpdateExistingBillWithUpdatedBillItem() { + Bill pendingBill = billService.getBill(2); + assertNotNull(pendingBill); + assertEquals(BillStatus.PENDING, pendingBill.getStatus()); + assertFalse(pendingBill.getLineItems().isEmpty()); + + BillLineItem firstItem = pendingBill.getLineItems().get(0); + BigDecimal updatedPrice = firstItem.getPrice().add(BigDecimal.TEN); + firstItem.setPrice(updatedPrice); + + billService.saveBill(pendingBill); + Context.flushSession(); + Context.clearSession(); + + Bill updatedBill = billService.getBill(2); + + assertEquals(pendingBill, updatedBill); + assertEquals(updatedPrice, updatedBill.getLineItems().get(0).getPrice()); + } + + /** + * @see org.openmrs.module.billing.api.impl.BillServiceImpl#getBill(Integer) + */ + @Test + public void getById_shouldReturnBillWithSpecifiedId() { + Bill bill = billService.getBill(1); + assertNotNull(bill); + assertEquals(1, bill.getId()); + } + + /** + * @see org.openmrs.module.billing.api.impl.BillServiceImpl#getBill(Integer) + */ + @Test + public void getById_shouldRemoveNullLineItems() { + Bill bill = billService.getBill(1); + assertNotNull(bill); + if (bill.getLineItems() != null) { + for (Object item : bill.getLineItems()) { + assertNotNull(item, "Line items should not contain null values"); + } + } + } + + /** + * @see org.openmrs.module.billing.api.impl.BillServiceImpl#saveBill(Bill) + */ + @Test + public void save_Bill_shouldAllowAddingLineItemsToPendingBill() { + // Get the PENDING bill from test data (bill_id=2) + Bill pendingBill = billService.getBill(2); + assertNotNull(pendingBill); + assertEquals(BillStatus.PENDING, pendingBill.getStatus()); + + // Add a new line item + BillLineItem newLineItem = new BillLineItem(); + newLineItem.setPrice(BigDecimal.valueOf(25.50)); + newLineItem.setQuantity(2); + newLineItem.setPaymentStatus(BillStatus.PENDING); + newLineItem.setLineItemOrder(pendingBill.getLineItems().size()); + pendingBill.addLineItem(newLineItem); + + // Should not throw exception + Bill savedBill = billService.saveBill(pendingBill); + assertNotNull(savedBill); + assertFalse(savedBill.getLineItems().isEmpty()); + } + + /** + * @see org.openmrs.module.billing.api.impl.BillServiceImpl#saveBill(Bill) + */ + @Test + public void save_Bill_shouldThrowExceptionWhenAddingLineItemsToPaidBill() { + // Get the PAID bill from test data (bill_id=1) + Bill paidBill = billService.getBill(1); + assertNotNull(paidBill); + assertEquals(BillStatus.PAID, paidBill.getStatus()); + + // Try to add a new line item + BillLineItem newLineItem = new BillLineItem(); + newLineItem.setPrice(BigDecimal.valueOf(25.50)); + newLineItem.setQuantity(2); + paidBill.addLineItem(newLineItem); + // Should throw exception + + assertThrows(IllegalArgumentException.class, () -> billService.saveBill(paidBill)); + } + + /** + * @see org.openmrs.module.billing.api.impl.BillServiceImpl#saveBill(Bill) + */ + @Test + public void save_Bill_shouldAllowRemovingLineItemsFromPendingBill() { + // Get the PENDING bill from test data (bill_id=2) + Bill pendingBill = billService.getBill(2); + assertNotNull(pendingBill); + assertEquals(BillStatus.PENDING, pendingBill.getStatus()); + + int originalSize = pendingBill.getLineItems().size(); + assertTrue(originalSize > 0); + + // Remove a line item + BillLineItem itemToRemove = pendingBill.getLineItems().get(0); + pendingBill.removeLineItem(itemToRemove); + + // Should not throw exception + Bill savedBill = billService.saveBill(pendingBill); + assertNotNull(savedBill); + assertTrue(savedBill.getLineItems().size() < originalSize); + } + + /** + * @see org.openmrs.module.billing.api.impl.BillServiceImpl#saveBill(Bill) + */ + @Test + public void save_Bill_shouldThrowExceptionWhenRemovingLineItemsFromPaidBill() { + // Get the POSTED bill from test data (bill_id=1) + Bill postedBill = billService.getBill(1); + assertNotNull(postedBill); + assertEquals(BillStatus.PAID, postedBill.getStatus()); + + BillLineItem itemToRemove = postedBill.getLineItems().get(0); + postedBill.removeLineItem(itemToRemove); + + // Should throw exception + assertThrows(IllegalArgumentException.class, () -> billService.saveBill(postedBill)); + } + + @Test + public void save_Bill_shouldNotThrowExceptionForPendingBill() { + Bill pendingBill = billService.getBill(2); + assertNotNull(pendingBill); + assertEquals(BillStatus.PENDING, pendingBill.getStatus()); + pendingBill.setReceiptNumber("ABV"); + assertDoesNotThrow(() -> billService.saveBill(pendingBill)); + } + + @Test + public void save_Bill_shouldThrowIllegalStateExceptionForPostedBill() { + Bill postedBill = billService.getBill(0); + assertNotNull(postedBill); + assertEquals(BillStatus.POSTED, postedBill.getStatus()); + + postedBill.setReceiptNumber("ABV"); + assertThrows(IllegalArgumentException.class, () -> billService.saveBill(postedBill)); + } + + /** + * @see org.openmrs.module.billing.api.impl.BillServiceImpl#getBillByUuid(String) + */ + @Test + public void getBillByUuid_shouldReturnBillWithSpecifiedUuid() { + Bill bill = billService.getBill(0); + assertNotNull(bill); + String uuid = bill.getUuid(); + + Bill foundBill = billService.getBillByUuid(uuid); + assertNotNull(foundBill); + assertEquals(uuid, foundBill.getUuid()); + assertEquals(0, foundBill.getId()); + } + + /** + * @see org.openmrs.module.billing.api.impl.BillServiceImpl#getBillByUuid(String) + */ + @Test + public void getBillByUuid_shouldReturnNullIfUuidNotFound() { + Bill bill = billService.getBillByUuid("nonexistent-uuid"); + assertNull(bill); + } + + /** + * @see org.openmrs.module.billing.api.impl.BillServiceImpl#getBills(BillSearch, PagingInfo) + */ + @Test + public void getBills_shouldReturnAllBillsWhenSearchIsEmpty() { + BillSearch billSearch = new BillSearch(); + List bills = billService.getBills(billSearch, null); + + assertNotNull(bills); + assertFalse(bills.isEmpty()); + } + + /** + * @see org.openmrs.module.billing.api.impl.BillServiceImpl#getBills(BillSearch, PagingInfo) + */ + @Test + public void getBills_shouldFilterByPatientUuid() { + Patient patient = patientService.getPatient(0); + assertNotNull(patient); + + BillSearch billSearch = new BillSearch(); + billSearch.setPatientUuid(patient.getUuid()); + + List bills = billService.getBills(billSearch, null); + assertNotNull(bills); + assertFalse(bills.isEmpty()); + + for (Bill bill : bills) { + assertEquals(patient.getUuid(), bill.getPatient().getUuid()); + } + } + + /** + * @see org.openmrs.module.billing.api.impl.BillServiceImpl#getBills(BillSearch, PagingInfo) + */ + @Test + public void getBills_shouldReturnEmptyListWhenSearchReturnsNoResults() { + BillSearch billSearch = new BillSearch(); + billSearch.setPatientUuid("nonexistent-uuid"); + + List bills = billService.getBills(billSearch, null); + assertNotNull(bills); + assertTrue(bills.isEmpty()); + } + + /** + * @see org.openmrs.module.billing.api.impl.BillServiceImpl#getBills(BillSearch, PagingInfo) + */ + @Test + public void getBills_shouldApplyPagingCorrectly() { + BillSearch billSearch = new BillSearch(); + PagingInfo pagingInfo = new PagingInfo(1, 2); + + List bills = billService.getBills(billSearch, pagingInfo); + assertNotNull(bills); + assertTrue(bills.size() <= 2); + assertNotNull(pagingInfo.getTotalRecordCount()); + } +} diff --git a/api/src/test/java/org/openmrs/module/billing/impl/CashPointServiceImplTest.java b/api/src/test/java/org/openmrs/module/billing/impl/CashPointServiceImplTest.java new file mode 100644 index 00000000..061cbc0f --- /dev/null +++ b/api/src/test/java/org/openmrs/module/billing/impl/CashPointServiceImplTest.java @@ -0,0 +1,190 @@ +/* + * The contents of this file are subject to the OpenMRS Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://license.openmrs.org + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the + * License for the specific language governing rights and limitations + * under the License. + * + * Copyright (C) OpenMRS, LLC. All Rights Reserved. + */ + +package org.openmrs.module.billing.impl; + +import java.util.List; + +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openmrs.Location; +import org.openmrs.api.LocationService; +import org.openmrs.api.context.Context; +import org.openmrs.module.billing.TestConstants; +import org.openmrs.module.billing.api.ICashPointService; +import org.openmrs.module.billing.api.model.CashPoint; +import org.openmrs.test.jupiter.BaseModuleContextSensitiveTest; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class CashPointServiceImplTest extends BaseModuleContextSensitiveTest { + + private ICashPointService cashPointService; + + private LocationService locationService; + + @BeforeEach + public void setup() { + cashPointService = Context.getService(ICashPointService.class); + locationService = Context.getLocationService(); + executeDataSet(TestConstants.CORE_DATASET2); + executeDataSet(TestConstants.BASE_DATASET_DIR + "CashPointTest.xml"); + } + + /** + * @see org.openmrs.module.billing.api.impl.CashPointServiceImpl#getCashPointsByLocation(Location, + * boolean) + */ + @Test + public void getCashPointsByLocation_shouldThrowIllegalArgumentExceptionIfLocationIsNull() { + assertThrows(IllegalArgumentException.class, () -> cashPointService.getCashPointsByLocation(null, false)); + } + + /** + * @see org.openmrs.module.billing.api.impl.CashPointServiceImpl#getCashPointsByLocation(Location, + * boolean) + */ + @Test + public void getCashPointsByLocation_shouldReturnCashPointsForLocation() { + Location location = locationService.getLocation(0); + assertNotNull(location); + List cashPoints = cashPointService.getCashPointsByLocation(location, false); + assertNotNull(cashPoints); + assertFalse(cashPoints.isEmpty()); + for (CashPoint cashPoint : cashPoints) { + assertEquals(location.getId(), cashPoint.getLocation().getId()); + } + } + + /** + * @see org.openmrs.module.billing.api.impl.CashPointServiceImpl#getCashPointsByLocation(Location, + * boolean) + */ + @Test + public void getCashPointsByLocation_shouldReturnEmptyListWhenLocationHasNoCashPoints() { + Location location = locationService.getLocation(999); + assertNotNull(location); + List cashPoints = cashPointService.getCashPointsByLocation(location, false); + assertNotNull(cashPoints); + assertTrue(cashPoints.isEmpty()); + } + + /** + * @see org.openmrs.module.billing.api.impl.CashPointServiceImpl#getCashPointsByLocationAndName(Location, + * String, boolean) + */ + @Test + public void getCashPointsByLocationAndName_shouldThrowIllegalArgumentExceptionIfLocationIsNull() { + assertThrows(IllegalArgumentException.class, + () -> cashPointService.getCashPointsByLocationAndName(null, "Test", false)); + } + + /** + * @see org.openmrs.module.billing.api.impl.CashPointServiceImpl#getCashPointsByLocationAndName(Location, + * String, boolean) + */ + @Test + public void getCashPointsByLocationAndName_shouldThrowIllegalArgumentExceptionIfNameIsNull() { + Location location = locationService.getLocation(0); + assertThrows(IllegalArgumentException.class, + () -> cashPointService.getCashPointsByLocationAndName(location, null, false)); + } + + /** + * @see org.openmrs.module.billing.api.impl.CashPointServiceImpl#getCashPointsByLocationAndName(Location, + * String, boolean) + */ + @Test + public void getCashPointsByLocationAndName_shouldThrowIllegalArgumentExceptionIfNameIsEmpty() { + Location location = locationService.getLocation(0); + assertThrows(IllegalArgumentException.class, + () -> cashPointService.getCashPointsByLocationAndName(location, "", false)); + } + + /** + * @see org.openmrs.module.billing.api.impl.CashPointServiceImpl#getCashPointsByLocationAndName(Location, + * String, boolean) + */ + @Test + public void getCashPointsByLocationAndName_shouldThrowIllegalArgumentExceptionIfNameIsTooLong() { + Location location = locationService.getLocation(0); + String longName = RandomStringUtils.randomAlphanumeric(256); + assertThrows(IllegalArgumentException.class, + () -> cashPointService.getCashPointsByLocationAndName(location, longName, false)); + } + + /** + * @see org.openmrs.module.billing.api.impl.CashPointServiceImpl#getCashPointsByLocationAndName(Location, + * String, boolean) + */ + @Test + public void getCashPointsByLocationAndName_shouldReturnCashPointsMatchingLocationAndName() { + Location location = locationService.getLocation(0); + List cashPoints = cashPointService.getCashPointsByLocationAndName(location, "Test", false); + assertNotNull(cashPoints); + assertFalse(cashPoints.isEmpty()); + for (CashPoint cashPoint : cashPoints) { + assertEquals(location.getId(), cashPoint.getLocation().getId()); + assertTrue(cashPoint.getName().startsWith("Test")); + } + } + + /** + * @see org.openmrs.module.billing.api.impl.CashPointServiceImpl#getCashPointsByLocationAndName(Location, + * String, boolean) + */ + @Test + public void getCashPointsByLocationAndName_shouldReturnEmptyListWhenNoMatch() { + Location location = locationService.getLocation(0); + List cashPoints = cashPointService.getCashPointsByLocationAndName(location, "Fake name", false); + assertNotNull(cashPoints); + assertTrue(cashPoints.isEmpty()); + } + + /** + * @see org.openmrs.module.billing.api.impl.CashPointServiceImpl#getById(int) + */ + @Test + public void getById_shouldReturnCashPointWithSpecifiedId() { + CashPoint cashPoint = cashPointService.getById(0); + assertNotNull(cashPoint); + assertEquals(0, cashPoint.getId()); + } + + /** + * @see org.openmrs.module.billing.api.impl.CashPointServiceImpl#getByUuid(String) + */ + @Test + public void getByUuid_shouldReturnCashPointWithSpecifiedUuid() { + CashPoint cashPoint = cashPointService.getByUuid("4028814B39BB04B90139BB04B98B0000"); + assertNotNull(cashPoint); + assertEquals("4028814B39BB04B90139BB04B98B0000", cashPoint.getUuid()); + } + + /** + * @see org.openmrs.module.billing.api.impl.CashPointServiceImpl#getAll() + */ + @Test + public void getAll_shouldReturnAllCashPoints() { + List cashPoints = cashPointService.getAll(); + assertNotNull(cashPoints); + assertFalse(cashPoints.isEmpty()); + assertEquals(7, cashPoints.size()); + } +} diff --git a/api/src/test/java/org/openmrs/module/billing/impl/CashierOptionsServiceGpImplTest.java b/api/src/test/java/org/openmrs/module/billing/impl/CashierOptionsServiceGpImplTest.java index 53517792..4501ecd3 100644 --- a/api/src/test/java/org/openmrs/module/billing/impl/CashierOptionsServiceGpImplTest.java +++ b/api/src/test/java/org/openmrs/module/billing/impl/CashierOptionsServiceGpImplTest.java @@ -1,198 +1,190 @@ -///* -// * The contents of this file are subject to the OpenMRS Public License -// * Version 1.1 (the "License"); you may not use this file except in -// * compliance with the License. You may obtain a copy of the License at -// * http://license.openmrs.org -// * -// * Software distributed under the License is distributed on an "AS IS" -// * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the -// * License for the specific language governing rights and limitations -// * under the License. -// * -// * Copyright (C) OpenMRS, LLC. All Rights Reserved. -// */ -//package org.openmrs.module.cashier.api.impl; -// -//import static org.junit.Assert.assertFalse; -//import static org.junit.Assert.assertNotNull; -//import static org.powermock.api.mockito.PowerMockito.mock; -//import static org.powermock.api.mockito.PowerMockito.when; -// -//import java.io.ByteArrayOutputStream; -// -//import org.apache.log4j.Appender; -//import org.apache.log4j.Layout; -//import org.apache.log4j.Logger; -//import org.apache.log4j.SimpleLayout; -//import org.apache.log4j.WriterAppender; -//import org.junit.Assert; -//import org.junit.Before; -//import org.junit.Test; -//import org.junit.runner.RunWith; -//import org.openmrs.api.AdministrationService; -//import org.openmrs.module.cashier.ModuleSettings; -//import org.openmrs.module.cashier.api.impl.CashierOptionsServiceGpImpl; -//import org.openmrs.module.cashier.api.model.CashierOptions; -//import org.openmrs.module.openhmis.inventory.api.IItemDataService; -//import org.openmrs.module.openhmis.inventory.api.model.Item; -//import org.powermock.modules.junit4.PowerMockRunner; -// -//@RunWith(PowerMockRunner.class) -//public class CashierOptionsServiceGpImplTest { -// private CashierOptionsServiceGpImpl optionsService = null; -// private AdministrationService adminService = null; -// private IItemDataService itemService = null; -// -// @Before -// public void before() { -// adminService = mock(AdministrationService.class); -// itemService = mock(IItemDataService.class); -// -// optionsService = new CashierOptionsServiceGpImpl(); -// } -// -// /** -// * @verifies load cashier options from the database -// * @see CashierOptionsServiceGpImpl#getOptions() -// */ -// @Test -// public void getOptions_shouldLoadCashierOptionsFromTheDatabase() throws Exception { -// when(adminService.getGlobalProperty(ModuleSettings.RECEIPT_REPORT_ID_PROPERTY)) -// .thenReturn("1"); -// when(adminService.getGlobalProperty(ModuleSettings.ROUNDING_MODE_PROPERTY)) -// .thenReturn(CashierOptions.RoundingMode.MID.toString()); -// when(adminService.getGlobalProperty(ModuleSettings.ROUND_TO_NEAREST_PROPERTY)) -// .thenReturn("5"); -// when(adminService.getGlobalProperty(ModuleSettings.ROUNDING_ITEM_ID)) -// .thenReturn("1"); -// when(adminService.getGlobalProperty(ModuleSettings.TIMESHEET_REQUIRED_PROPERTY)) -// .thenReturn("true"); -// -// Item item = new Item(); -// when(itemService.getById(1)) -// .thenReturn(item); -// -// CashierOptions options = optionsService.getOptions(); -// -// Assert.assertNotNull(options); -// Assert.assertEquals(1, options.getDefaultReceiptReportId()); -// Assert.assertEquals(CashierOptions.RoundingMode.MID, options.getRoundingMode()); -// Assert.assertEquals(5, (int)options.getRoundToNearest()); -// Assert.assertEquals(item.getUuid(), options.getRoundingItemUuid()); -// Assert.assertEquals(true, options.isTimesheetRequired()); -// } -// -// /** -// * @verifies not throw exception if numeric options are null -// * @see CashierOptionsServiceGpImpl#getOptions() -// */ -// @Test -// public void getOptions_shouldNotThrowExceptionIfNumericOptionsAreNull() throws Exception { -// when(adminService.getGlobalProperty(ModuleSettings.RECEIPT_REPORT_ID_PROPERTY)) -// .thenReturn(null); -// when(adminService.getGlobalProperty(ModuleSettings.ROUNDING_MODE_PROPERTY)) -// .thenReturn(null); -// when(adminService.getGlobalProperty(ModuleSettings.ROUND_TO_NEAREST_PROPERTY)) -// .thenReturn(null); -// when(adminService.getGlobalProperty(ModuleSettings.ROUNDING_ITEM_ID)) -// .thenReturn(null); -// when(adminService.getGlobalProperty(ModuleSettings.TIMESHEET_REQUIRED_PROPERTY)) -// .thenReturn(null); -// -// CashierOptions options = optionsService.getOptions(); -// -// Assert.assertNotNull(options); -// } -// -// /** -// * @verifies default to false if timesheet required is not specified -// * @see CashierOptionsServiceGpImpl#getOptions() -// */ -// @Test -// public void getOptions_shouldDefaultToFalseIfTimesheetRequiredIsNotSpecified() throws Exception { -// when(adminService.getGlobalProperty(ModuleSettings.RECEIPT_REPORT_ID_PROPERTY)) -// .thenReturn(null); -// when(adminService.getGlobalProperty(ModuleSettings.ROUNDING_MODE_PROPERTY)) -// .thenReturn(null); -// when(adminService.getGlobalProperty(ModuleSettings.ROUND_TO_NEAREST_PROPERTY)) -// .thenReturn(null); -// when(adminService.getGlobalProperty(ModuleSettings.ROUNDING_ITEM_ID)) -// .thenReturn(null); -// when(adminService.getGlobalProperty(ModuleSettings.TIMESHEET_REQUIRED_PROPERTY)) -// .thenReturn(null); -// -// CashierOptions options = optionsService.getOptions(); -// -// Assert.assertNotNull(options); -// Assert.assertEquals(false, options.isTimesheetRequired()); -// } -// -// /** -// * @verifies log Error if Exception due to non-parsable rounding item id -// * @see CashierOptionsServiceGpImpl#getOptions() -// */ -// @Test -// public void getOptions_shouldLogErrorIfRoundingItemIdCannotBeParsed() throws Exception { -// -// when(adminService.getGlobalProperty(ModuleSettings.RECEIPT_REPORT_ID_PROPERTY)) -// .thenReturn(null); -// when(adminService.getGlobalProperty(ModuleSettings.ROUNDING_MODE_PROPERTY)) -// .thenReturn(CashierOptions.RoundingMode.FLOOR.toString()); -// when(adminService.getGlobalProperty(ModuleSettings.ROUND_TO_NEAREST_PROPERTY)) -// .thenReturn("5"); -// when(adminService.getGlobalProperty(ModuleSettings.ROUNDING_ITEM_ID)) -// .thenReturn("HELP"); -// -// Logger logger = Logger.getLogger(CashierOptionsServiceGpImpl.class); -// -// ByteArrayOutputStream out = new ByteArrayOutputStream(); -// Layout layout = new SimpleLayout(); -// Appender appender = new WriterAppender(layout, out); -// logger.addAppender(appender); -// -// try { -// optionsService.getOptions(); -// String logMsg = out.toString(); -// assertNotNull(logMsg); -// assertFalse((logMsg.trim()).equals("")); -// } finally { -// logger.removeAppender(appender); -// } -// } -// -// /** -// * @verifies log error if rouding item id is set but item cannot be found (and hence is null) -// * @see CashierOptionsServiceGpImpl#getOptions() -// */ -// @Test -// public void getOptions_shouldLogErrorIfRoundingItemIsNullDespiteIdGiven() throws Exception { -// -// when(adminService.getGlobalProperty(ModuleSettings.RECEIPT_REPORT_ID_PROPERTY)) -// .thenReturn(null); -// when(adminService.getGlobalProperty(ModuleSettings.ROUNDING_MODE_PROPERTY)) -// .thenReturn(CashierOptions.RoundingMode.FLOOR.toString()); -// when(adminService.getGlobalProperty(ModuleSettings.ROUND_TO_NEAREST_PROPERTY)) -// .thenReturn("5"); -// when(adminService.getGlobalProperty(ModuleSettings.ROUNDING_ITEM_ID)) -// .thenReturn("273423"); -// when(adminService.getGlobalProperty(ModuleSettings.TIMESHEET_REQUIRED_PROPERTY)) -// .thenReturn(null); -// -// Logger logger = Logger.getLogger(CashierOptionsServiceGpImpl.class); -// -// ByteArrayOutputStream out = new ByteArrayOutputStream(); -// Layout layout = new SimpleLayout(); -// Appender appender = new WriterAppender(layout, out); -// logger.addAppender(appender); -// -// try { -// optionsService.getOptions(); -// String logMsg = out.toString(); -// assertNotNull(logMsg); -// assertFalse((logMsg.trim()).equals("")); -// } finally { -// logger.removeAppender(appender); -// } -// } -// -//} +/* + * The contents of this file are subject to the OpenMRS Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://license.openmrs.org + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the + * License for the specific language governing rights and limitations + * under the License. + * + * Copyright (C) OpenMRS, LLC. All Rights Reserved. + */ + +package org.openmrs.module.billing.impl; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openmrs.api.AdministrationService; +import org.openmrs.api.context.Context; +import org.openmrs.module.billing.ModuleSettings; +import org.openmrs.module.billing.api.impl.CashierOptionsServiceGpImpl; +import org.openmrs.module.billing.api.model.CashierOptions; +import org.openmrs.test.jupiter.BaseModuleContextSensitiveTest; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class CashierOptionsServiceGpImplTest extends BaseModuleContextSensitiveTest { + + private CashierOptionsServiceGpImpl service; + + private AdministrationService adminService; + + @BeforeEach + public void setup() { + service = new CashierOptionsServiceGpImpl(); + adminService = Context.getAdministrationService(); + } + + /** + * @see org.openmrs.module.billing.api.impl.CashierOptionsServiceGpImpl#getOptions() + */ + @Test + public void getOptions_shouldReturnCashierOptionsWithDefaults() { + CashierOptions options = service.getOptions(); + assertNotNull(options); + assertFalse(options.isTimesheetRequired()); + assertEquals(CashierOptions.RoundingMode.MID, options.getRoundingMode()); + assertEquals(0, options.getRoundToNearest()); + } + + /** + * @see org.openmrs.module.billing.api.impl.CashierOptionsServiceGpImpl#getOptions() + */ + @Test + public void getOptions_shouldLoadDefaultReceiptReportIdFromGlobalProperty() { + adminService.setGlobalProperty(ModuleSettings.RECEIPT_REPORT_ID_PROPERTY, "123"); + + CashierOptions options = service.getOptions(); + assertNotNull(options); + assertEquals(123, options.getDefaultReceiptReportId()); + } + + /** + * @see org.openmrs.module.billing.api.impl.CashierOptionsServiceGpImpl#getOptions() + */ + @Test + public void getOptions_shouldHandleInvalidReceiptReportId() { + adminService.setGlobalProperty(ModuleSettings.RECEIPT_REPORT_ID_PROPERTY, "invalid"); + + CashierOptions options = service.getOptions(); + assertNotNull(options); + assertEquals(0, options.getDefaultReceiptReportId()); + } + + /** + * @see org.openmrs.module.billing.api.impl.CashierOptionsServiceGpImpl#getOptions() + */ + @Test + public void getOptions_shouldLoadTimesheetRequiredFromGlobalProperty() { + adminService.setGlobalProperty(ModuleSettings.TIMESHEET_REQUIRED_PROPERTY, "true"); + + CashierOptions options = service.getOptions(); + assertNotNull(options); + assertTrue(options.isTimesheetRequired()); + } + + /** + * @see org.openmrs.module.billing.api.impl.CashierOptionsServiceGpImpl#getOptions() + */ + @Test + public void getOptions_shouldDefaultToFalseIfTimesheetRequiredIsNotSpecified() { + adminService.setGlobalProperty(ModuleSettings.TIMESHEET_REQUIRED_PROPERTY, ""); + + CashierOptions options = service.getOptions(); + assertNotNull(options); + assertFalse(options.isTimesheetRequired()); + } + + /** + * @see org.openmrs.module.billing.api.impl.CashierOptionsServiceGpImpl#getOptions() + */ + @Test + public void getOptions_shouldHandleInvalidTimesheetRequiredValue() { + adminService.setGlobalProperty(ModuleSettings.TIMESHEET_REQUIRED_PROPERTY, "invalid"); + + CashierOptions options = service.getOptions(); + assertNotNull(options); + assertFalse(options.isTimesheetRequired()); + } + + /** + * @see org.openmrs.module.billing.api.impl.CashierOptionsServiceGpImpl#getOptions() + */ + @Test + public void getOptions_shouldSetDefaultRoundingOptionsWhenRoundingItemUuidIsEmpty() { + CashierOptions options = service.getOptions(); + assertNotNull(options); + assertEquals(CashierOptions.RoundingMode.MID, options.getRoundingMode()); + assertEquals(0, options.getRoundToNearest()); + } + + /** + * @see org.openmrs.module.billing.api.impl.CashierOptionsServiceGpImpl#getOptions() + */ + @Test + public void getOptions_shouldNotThrowExceptionIfNumericOptionsAreNull() { + adminService.setGlobalProperty(ModuleSettings.RECEIPT_REPORT_ID_PROPERTY, ""); + adminService.setGlobalProperty(ModuleSettings.ROUND_TO_NEAREST_PROPERTY, ""); + + assertDoesNotThrow(() -> { + CashierOptions options = service.getOptions(); + assertNotNull(options); + }); + } + + /** + * @see org.openmrs.module.billing.api.impl.CashierOptionsServiceGpImpl#getOptions() + */ + @Test + public void getOptions_shouldHandleMultiplePropertiesSet() { + adminService.setGlobalProperty(ModuleSettings.RECEIPT_REPORT_ID_PROPERTY, "456"); + adminService.setGlobalProperty(ModuleSettings.TIMESHEET_REQUIRED_PROPERTY, "true"); + + CashierOptions options = service.getOptions(); + assertNotNull(options); + assertEquals(456, options.getDefaultReceiptReportId()); + assertTrue(options.isTimesheetRequired()); + } + + /** + * @see org.openmrs.module.billing.api.impl.CashierOptionsServiceGpImpl#getOptions() + */ + @Test + public void getOptions_shouldLoadCashierOptionsFromTheDatabase() { + adminService.setGlobalProperty(ModuleSettings.RECEIPT_REPORT_ID_PROPERTY, "1"); + adminService.setGlobalProperty(ModuleSettings.TIMESHEET_REQUIRED_PROPERTY, "true"); + + CashierOptions options = service.getOptions(); + + assertNotNull(options); + assertEquals(1, options.getDefaultReceiptReportId()); + assertEquals(CashierOptions.RoundingMode.MID, options.getRoundingMode()); + assertEquals(0, options.getRoundToNearest()); + assertTrue(options.isTimesheetRequired()); + } + + /** + * @see org.openmrs.module.billing.api.impl.CashierOptionsServiceGpImpl#getOptions() + */ + @Test + public void getOptions_shouldHandleNullGlobalProperties() { + adminService.setGlobalProperty(ModuleSettings.RECEIPT_REPORT_ID_PROPERTY, null); + adminService.setGlobalProperty(ModuleSettings.ROUNDING_MODE_PROPERTY, null); + adminService.setGlobalProperty(ModuleSettings.ROUND_TO_NEAREST_PROPERTY, null); + adminService.setGlobalProperty(ModuleSettings.TIMESHEET_REQUIRED_PROPERTY, null); + + CashierOptions options = service.getOptions(); + + assertNotNull(options); + assertEquals(0, options.getDefaultReceiptReportId()); + assertEquals(CashierOptions.RoundingMode.MID, options.getRoundingMode()); + assertEquals(0, options.getRoundToNearest()); + assertFalse(options.isTimesheetRequired()); + } +} diff --git a/api/src/test/java/org/openmrs/module/billing/validator/BillValidatorTest.java b/api/src/test/java/org/openmrs/module/billing/validator/BillValidatorTest.java new file mode 100644 index 00000000..bc445c10 --- /dev/null +++ b/api/src/test/java/org/openmrs/module/billing/validator/BillValidatorTest.java @@ -0,0 +1,77 @@ +/* + * The contents of this file are subject to the OpenMRS Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://license.openmrs.org + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the + * License for the specific language governing rights and limitations + * under the License. + * + * Copyright (C) OpenMRS, LLC. All Rights Reserved. + */ + +package org.openmrs.module.billing.validator; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openmrs.api.context.Context; +import org.openmrs.module.billing.TestConstants; +import org.openmrs.module.billing.api.BillService; +import org.openmrs.module.billing.api.model.Bill; +import org.openmrs.module.billing.api.model.BillStatus; +import org.openmrs.test.jupiter.BaseModuleContextSensitiveTest; +import org.springframework.validation.BindException; +import org.springframework.validation.Errors; + +/** + * Integration tests for {@link BillValidator} + */ +public class BillValidatorTest extends BaseModuleContextSensitiveTest { + + private BillValidator billValidator; + + private BillService billService; + + @BeforeEach + public void setup() throws Exception { + billValidator = new BillValidator(); + billService = Context.getService(BillService.class); + + executeDataSet(TestConstants.CORE_DATASET2); + executeDataSet(TestConstants.BASE_DATASET_DIR + "StockOperationType.xml"); + executeDataSet(TestConstants.BASE_DATASET_DIR + "PaymentModeTest.xml"); + executeDataSet(TestConstants.BASE_DATASET_DIR + "CashPointTest.xml"); + executeDataSet(TestConstants.BASE_DATASET_DIR + "BillTest.xml"); + } + + @Test + public void validate_shouldNotRejectPendingBill() { + Bill pendingBill = billService.getBill(2); + assertNotNull(pendingBill); + assertEquals(BillStatus.PENDING, pendingBill.getStatus()); + + Errors errors = new BindException(pendingBill, "bill"); + billValidator.validate(pendingBill, errors); + + assertFalse(errors.hasErrors()); + } + + @Test + public void validate_shouldRejectPaidBill() { + Bill paidBill = billService.getBill(1); + assertNotNull(paidBill); + assertEquals(BillStatus.PAID, paidBill.getStatus()); + + Errors errors = new BindException(paidBill, "bill"); + billValidator.validate(paidBill, errors); + + assertTrue(errors.hasErrors()); + assertTrue(errors.getGlobalError().getDefaultMessage() + .contains("Bill can only be modified when the bill is in PENDING state")); + assertTrue(errors.getGlobalError().getDefaultMessage().contains("PAID")); + } +} diff --git a/api/src/test/resources/org/openmrs/module/billing/api/include/BillExemptionTest.xml b/api/src/test/resources/org/openmrs/module/billing/api/include/BillExemptionTest.xml new file mode 100644 index 00000000..5844d760 --- /dev/null +++ b/api/src/test/resources/org/openmrs/module/billing/api/include/BillExemptionTest.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/api/src/test/resources/org/openmrs/module/billing/api/include/BillTest.xml b/api/src/test/resources/org/openmrs/module/billing/api/include/BillTest.xml index a4d909aa..b2fb47b7 100644 --- a/api/src/test/resources/org/openmrs/module/billing/api/include/BillTest.xml +++ b/api/src/test/resources/org/openmrs/module/billing/api/include/BillTest.xml @@ -1,19 +1,45 @@ + + + + + + + + + + + + + + + + + + diff --git a/api/src/test/resources/org/openmrs/module/billing/api/include/CoreTest-2.0.xml b/api/src/test/resources/org/openmrs/module/billing/api/include/CoreTest-2.0.xml index 382abef9..88961198 100644 --- a/api/src/test/resources/org/openmrs/module/billing/api/include/CoreTest-2.0.xml +++ b/api/src/test/resources/org/openmrs/module/billing/api/include/CoreTest-2.0.xml @@ -27,6 +27,9 @@ uuid="ef93c695-ac43-450a-93f8-4b2b4d50a3c8"/> + + + + + + + + + + + + + + + diff --git a/fhir/pom.xml b/fhir/pom.xml index b9c1c647..57b9d0bb 100644 --- a/fhir/pom.xml +++ b/fhir/pom.xml @@ -6,7 +6,7 @@ org.openmrs.module billing - 1.3.3-SNAPSHOT + 2.0.0-SNAPSHOT billing-fhir @@ -54,6 +54,22 @@ fhir2-api test-jar + + + org.openmrs.module + fhir2-api-2.5 + + + + org.openmrs.module + fhir2-api-2.6 + + + + org.openmrs.module + fhir2-api-2.7 + + org.openmrs.module stockmanagement-api diff --git a/fhir/src/test/java/org/openmrs/module/billing/impl/FhirInvoiceServiceImplTest.java b/fhir/src/test/java/org/openmrs/module/billing/impl/FhirInvoiceServiceImplTest.java index e9d0f1db..1bababb3 100644 --- a/fhir/src/test/java/org/openmrs/module/billing/impl/FhirInvoiceServiceImplTest.java +++ b/fhir/src/test/java/org/openmrs/module/billing/impl/FhirInvoiceServiceImplTest.java @@ -15,7 +15,6 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.notNullValue; -import static org.hamcrest.Matchers.nullValue; import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) diff --git a/lombok.config b/lombok.config new file mode 100644 index 00000000..ec478262 --- /dev/null +++ b/lombok.config @@ -0,0 +1,3 @@ +# Lombok configuration for openmrs-module-billing + +lombok.getter.noIsPrefix = true \ No newline at end of file diff --git a/omod/pom.xml b/omod/pom.xml index 2ff62aea..34610b20 100644 --- a/omod/pom.xml +++ b/omod/pom.xml @@ -4,7 +4,7 @@ org.openmrs.module billing - 1.3.3-SNAPSHOT + 2.0.0-SNAPSHOT billing-omod diff --git a/omod/src/main/java/org/openmrs/module/billing/web/base/resource/BaseRestDataResource.java b/omod/src/main/java/org/openmrs/module/billing/web/base/resource/BaseRestDataResource.java index c4abaf39..4590b1ba 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/base/resource/BaseRestDataResource.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/base/resource/BaseRestDataResource.java @@ -59,12 +59,7 @@ public abstract class BaseRestDataResource extends DataDe * @param The {@link OpenmrsObject} stored in the collection. */ public static void syncCollection(Collection base, Collection sync) { - syncCollection(base, sync, new Action2, E>() { - @Override - public void apply(Collection collection, E entity) { - collection.add(entity); - } - }, new Action2, E>() { + syncCollection(base, sync, (collection, entity) -> collection.add(entity), new Action2, E>() { @Override public void apply(Collection collection, E entity) { collection.remove(entity); @@ -112,7 +107,7 @@ public static void syncCollection(Collection base, @Override public E save(E delegate) { - return getService().save(delegate); + return getService().saveBill(delegate); } @Override diff --git a/omod/src/main/java/org/openmrs/module/billing/web/base/resource/BaseRestMetadataResource.java b/omod/src/main/java/org/openmrs/module/billing/web/base/resource/BaseRestMetadataResource.java index b81a8076..817ded0d 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/base/resource/BaseRestMetadataResource.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/base/resource/BaseRestMetadataResource.java @@ -66,7 +66,7 @@ public abstract class BaseRestMetadataResource extend @Override public E save(E entity) { try { - return getService().save(entity); + return getService().saveBill(entity); } catch (PrivilegeException p) { LOG.error("Exception occured when trying to save entity <" + entity.getName() + "> as privilege is missing", p); throw new PrivilegeException("Can't save entity with name <" + entity.getName() + "> as privilege is missing"); diff --git a/omod/src/main/java/org/openmrs/module/billing/web/base/resource/BaseRestObjectResource.java b/omod/src/main/java/org/openmrs/module/billing/web/base/resource/BaseRestObjectResource.java index 58299c2b..64be3845 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/base/resource/BaseRestObjectResource.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/base/resource/BaseRestObjectResource.java @@ -71,7 +71,7 @@ public E save(E delegate) { } IObjectDataService service = Context.getService(clazz); - service.save(delegate); + service.saveBill(delegate); return delegate; } diff --git a/omod/src/main/java/org/openmrs/module/billing/web/legacyweb/controller/AbstractSequentialReceiptNumberGenerator.java b/omod/src/main/java/org/openmrs/module/billing/web/legacyweb/controller/AbstractSequentialReceiptNumberGenerator.java index 66c5fa3d..c0aaecef 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/legacyweb/controller/AbstractSequentialReceiptNumberGenerator.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/legacyweb/controller/AbstractSequentialReceiptNumberGenerator.java @@ -62,7 +62,7 @@ public String post(@ModelAttribute("generator") SequentialReceiptNumberGenerator } // Save the generator settings - getService().save(generator); + getService().saveBill(generator); // Set the system generator ReceiptNumberGeneratorFactory.setGenerator(new SequentialReceiptNumberGenerator()); diff --git a/omod/src/main/java/org/openmrs/module/billing/web/legacyweb/controller/BillAddEditController.java b/omod/src/main/java/org/openmrs/module/billing/web/legacyweb/controller/BillAddEditController.java index 642023d7..564dda31 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/legacyweb/controller/BillAddEditController.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/legacyweb/controller/BillAddEditController.java @@ -26,7 +26,7 @@ import org.openmrs.api.PatientService; import org.openmrs.api.context.Context; import org.openmrs.module.billing.ModuleSettings; -import org.openmrs.module.billing.api.IBillService; +import org.openmrs.module.billing.api.BillService; import org.openmrs.module.billing.api.ICashierOptionsService; import org.openmrs.module.billing.api.base.util.UrlUtil; import org.openmrs.module.billing.api.model.Bill; @@ -159,18 +159,17 @@ private void addBillAttributes(ModelMap model, Bill bill, Patient patient) { model.addAttribute("patient", patient); model.addAttribute("cashPoint", bill.getCashPoint()); model.addAttribute("adjustmentReason", bill.getAdjustmentReason()); - if (!bill.isReceiptPrinted() - || (bill.isReceiptPrinted() && Context.hasPrivilege(PrivilegeConstants.REPRINT_RECEIPT))) { + if (!bill.getReceiptPrinted() || Context.hasPrivilege(PrivilegeConstants.REPRINT_RECEIPT)) { model.addAttribute("showPrint", true); } } private Bill getBillFromService(String billUuid) { - IBillService service = Context.getService(IBillService.class); + BillService service = Context.getService(BillService.class); Bill bill; try { - bill = service.getByUuid(billUuid); + bill = service.getBillByUuid(billUuid); } catch (APIException e) { LOG.error("Error when trying to get bill with ID <" + billUuid + ">", e); throw new APIException("Error when trying to get bill with ID <" + billUuid + ">"); diff --git a/omod/src/main/java/org/openmrs/module/billing/web/legacyweb/controller/CashierController.java b/omod/src/main/java/org/openmrs/module/billing/web/legacyweb/controller/CashierController.java index f73b7d70..2621761a 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/legacyweb/controller/CashierController.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/legacyweb/controller/CashierController.java @@ -127,7 +127,7 @@ public String post(Timesheet timesheet, Errors errors, WebRequest request, Model return null; } - Context.getService(ITimesheetService.class).save(timesheet); + Context.getService(ITimesheetService.class).saveBill(timesheet); if (StringUtils.isEmpty(returnUrl)) { returnUrl = "redirect:"; diff --git a/omod/src/main/java/org/openmrs/module/billing/web/legacyweb/controller/PatientBillHistoryController.java b/omod/src/main/java/org/openmrs/module/billing/web/legacyweb/controller/PatientBillHistoryController.java index 320308aa..36f156d3 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/legacyweb/controller/PatientBillHistoryController.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/legacyweb/controller/PatientBillHistoryController.java @@ -17,7 +17,7 @@ import org.apache.log4j.Logger; import org.openmrs.api.context.Context; -import org.openmrs.module.billing.api.IBillService; +import org.openmrs.module.billing.api.BillService; import org.openmrs.module.billing.api.model.Bill; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; @@ -33,14 +33,10 @@ public class PatientBillHistoryController { private static final Logger LOG = Logger.getLogger(PatientBillHistoryController.class); - public PatientBillHistoryController() { - - } - @RequestMapping(method = RequestMethod.GET) - public void billHistory(ModelMap model, @RequestParam(value = "patientId", required = true) int patientId) { + public void billHistory(ModelMap model, @RequestParam(value = "patientUuid") String patientUuid) { LOG.warn("In bill history controller"); - List bills = Context.getService(IBillService.class).getBillsByPatientId(patientId, null); + List bills = Context.getService(BillService.class).getBillsByPatientUuid(patientUuid, null); model.addAttribute("bills", bills); } } diff --git a/omod/src/main/java/org/openmrs/module/billing/web/legacyweb/filter/CashierLogoutFilter.java b/omod/src/main/java/org/openmrs/module/billing/web/legacyweb/filter/CashierLogoutFilter.java index ce3bef7f..56ed08cb 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/legacyweb/filter/CashierLogoutFilter.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/legacyweb/filter/CashierLogoutFilter.java @@ -81,7 +81,7 @@ private void clockOutCashier() { if (cashierIsClockedIn(timesheet)) { timesheet.setClockOut(new Date()); - timesheetService.save(timesheet); + timesheetService.saveBill(timesheet); } } diff --git a/omod/src/main/java/org/openmrs/module/billing/web/rest/controller/CashierRestController.java b/omod/src/main/java/org/openmrs/module/billing/web/rest/controller/CashierRestController.java index d9c2f36b..3d720d1e 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/rest/controller/CashierRestController.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/rest/controller/CashierRestController.java @@ -35,7 +35,7 @@ public Object get(@RequestBody BillableServiceMapper request) { BillableService billableService = request.billableServiceMapper(request); IBillableItemsService service = Context.getService(IBillableItemsService.class); - service.save(billableService); + service.saveBill(billableService); return true; } diff --git a/omod/src/main/java/org/openmrs/module/billing/web/rest/controller/ReceiptController.java b/omod/src/main/java/org/openmrs/module/billing/web/rest/controller/ReceiptController.java index d61a4993..d50fb53e 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/rest/controller/ReceiptController.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/rest/controller/ReceiptController.java @@ -16,7 +16,7 @@ import java.io.IOException; import org.openmrs.api.context.Context; -import org.openmrs.module.billing.api.IBillService; +import org.openmrs.module.billing.api.BillService; import org.openmrs.module.billing.api.model.Bill; import org.openmrs.module.webservices.rest.web.RestConstants; import org.openmrs.module.webservices.rest.web.v1_0.controller.BaseRestController; @@ -37,22 +37,25 @@ public class ReceiptController extends BaseRestController { @RequestMapping(method = RequestMethod.GET) - public ResponseEntity get(@RequestParam(value = "billId", required = false) Integer billId) throws IOException { - IBillService service = Context.getService(IBillService.class); - Bill bill = service.getById(billId); + public ResponseEntity get(@RequestParam(value = "billUuid", required = false) String billUuid) + throws IOException { + BillService service = Context.getService(BillService.class); + Bill bill = service.getBillByUuid(billUuid); if (bill == null) { - return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + return new ResponseEntity<>(HttpStatus.NOT_FOUND); } byte[] pdfFile = service.downloadBillReceipt(bill); - if (pdfFile.length > 0) { + if (pdfFile != null && pdfFile.length > 0) { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_PDF); + headers.setContentLength(pdfFile.length); + headers.add(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"receipt-" + bill.getId() + ".pdf\""); return new ResponseEntity<>(pdfFile, headers, HttpStatus.OK); } else { - return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); } } diff --git a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillExemptionResource.java b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillExemptionResource.java new file mode 100644 index 00000000..62c94116 --- /dev/null +++ b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillExemptionResource.java @@ -0,0 +1,142 @@ +/* + * The contents of this file are subject to the OpenMRS Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://license.openmrs.org + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the + * License for the specific language governing rights and limitations + * under the License. + * + * Copyright (C) OpenMRS, LLC. All Rights Reserved. + */ +package org.openmrs.module.billing.web.rest.resource; + +import org.openmrs.api.context.Context; +import org.openmrs.module.billing.api.BillExemptionService; +import org.openmrs.module.billing.api.model.BillExemption; +import org.openmrs.module.billing.api.model.BillExemptionRule; +import org.openmrs.module.billing.api.model.ExemptionType; +import org.openmrs.module.billing.web.rest.controller.base.CashierResourceController; +import org.openmrs.module.webservices.rest.web.RequestContext; +import org.openmrs.module.webservices.rest.web.RestConstants; +import org.openmrs.module.webservices.rest.web.annotation.PropertySetter; +import org.openmrs.module.webservices.rest.web.annotation.Resource; +import org.openmrs.module.webservices.rest.web.representation.DefaultRepresentation; +import org.openmrs.module.webservices.rest.web.representation.FullRepresentation; +import org.openmrs.module.webservices.rest.web.representation.RefRepresentation; +import org.openmrs.module.webservices.rest.web.representation.Representation; +import org.openmrs.module.webservices.rest.web.resource.impl.DelegatingResourceDescription; +import org.openmrs.module.webservices.rest.web.resource.impl.MetadataDelegatingCrudResource; +import org.openmrs.module.webservices.rest.web.response.ResponseException; + +import java.util.List; + +/** + * REST resource representing a {@link BillExemption}. + */ +@Resource(name = RestConstants.VERSION_1 + CashierResourceController.BILLING_NAMESPACE + "/billExemption", + supportedClass = BillExemption.class, supportedOpenmrsVersions = {"2.0 - 2.*"}) +public class BillExemptionResource extends MetadataDelegatingCrudResource { + + @Override + public BillExemption newDelegate() { + return new BillExemption(); + } + + @Override + public BillExemption save(BillExemption delegate) { + return getService().save(delegate); + } + + @Override + public BillExemption getByUniqueId(String uniqueId) { + return getService().getBillingExemptionByUuid(uniqueId); + } + + @Override + public void delete(BillExemption delegate, String reason, RequestContext context) throws ResponseException { + if (delegate.getRetired()) { + return; + } + delegate.setRetired(true); + delegate.setRetireReason(reason); + getService().save(delegate); + } + + @Override + public void purge(BillExemption delegate, RequestContext context) throws ResponseException { + throw new UnsupportedOperationException("Purge is not supported for BillingExemption"); + } + + @Override + public DelegatingResourceDescription getRepresentationDescription(Representation rep) { + DelegatingResourceDescription description = new DelegatingResourceDescription(); + + if (rep instanceof RefRepresentation) { + description.addProperty("uuid"); + description.addProperty("name"); + description.addProperty("description"); + description.addProperty("retired"); + } else if (rep instanceof DefaultRepresentation) { + description.addProperty("uuid"); + description.addProperty("name"); + description.addProperty("description"); + description.addProperty("retired"); + description.addProperty("retireReason"); + description.addProperty("concept", Representation.REF); + description.addProperty("exemptionType"); + description.addProperty("rules", Representation.DEFAULT); + } else if (rep instanceof FullRepresentation) { + description.addProperty("uuid"); + description.addProperty("name"); + description.addProperty("description"); + description.addProperty("retired"); + description.addProperty("retireReason"); + description.addProperty("concept", Representation.DEFAULT); + description.addProperty("exemptionType"); + description.addProperty("rules", Representation.FULL); + description.addProperty("auditInfo"); + } + + return description; + } + + @Override + public DelegatingResourceDescription getCreatableProperties() { + DelegatingResourceDescription description = new DelegatingResourceDescription(); + description.addProperty("name"); + description.addProperty("description"); + description.addProperty("concept"); + description.addProperty("exemptionType"); + description.addProperty("rules"); + return description; + } + + @Override + public DelegatingResourceDescription getUpdatableProperties() { + return getCreatableProperties(); + } + + @PropertySetter("rules") + public void setRules(BillExemption instance, List rules) { + if (rules != null) { + for (BillExemptionRule rule : rules) { + rule.setBillingExemption(instance); + } + instance.setRules(rules); + } + } + + @PropertySetter("exemptionType") + public void setExemptionType(BillExemption instance, String exemptionType) { + if (exemptionType != null) { + instance.setExemptionType(ExemptionType.valueOf(exemptionType)); + } + } + + private BillExemptionService getService() { + return Context.getService(BillExemptionService.class); + } +} \ No newline at end of file diff --git a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillExemptionRuleResource.java b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillExemptionRuleResource.java new file mode 100644 index 00000000..cf7c3ccc --- /dev/null +++ b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillExemptionRuleResource.java @@ -0,0 +1,161 @@ +/* + * The contents of this file are subject to the OpenMRS Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://license.openmrs.org + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the + * License for the specific language governing rights and limitations + * under the License. + * + * Copyright (C) OpenMRS, LLC. All Rights Reserved. + */ +package org.openmrs.module.billing.web.rest.resource; + +import org.apache.commons.lang.StringEscapeUtils; +import org.openmrs.api.context.Context; +import org.openmrs.module.billing.api.BillExemptionService; +import org.openmrs.module.billing.api.evaluator.ScriptType; +import org.openmrs.module.billing.api.model.BillExemption; +import org.openmrs.module.billing.api.model.BillExemptionRule; +import org.openmrs.module.webservices.rest.web.RequestContext; +import org.openmrs.module.webservices.rest.web.annotation.PropertySetter; +import org.openmrs.module.webservices.rest.web.annotation.SubResource; +import org.openmrs.module.webservices.rest.web.representation.DefaultRepresentation; +import org.openmrs.module.webservices.rest.web.representation.FullRepresentation; +import org.openmrs.module.webservices.rest.web.representation.RefRepresentation; +import org.openmrs.module.webservices.rest.web.representation.Representation; +import org.openmrs.module.webservices.rest.web.resource.api.PageableResult; +import org.openmrs.module.webservices.rest.web.resource.impl.DelegatingResourceDescription; +import org.openmrs.module.webservices.rest.web.resource.impl.DelegatingSubResource; +import org.openmrs.module.webservices.rest.web.resource.impl.NeedsPaging; +import org.openmrs.module.webservices.rest.web.response.ResourceDoesNotSupportOperationException; +import org.openmrs.module.webservices.rest.web.response.ResponseException; + +import java.util.ArrayList; +import java.util.List; + +/** + * REST sub-resource representing a {@link BillExemptionRule}. + */ +@SubResource(parent = BillExemptionResource.class, path = "rule", supportedClass = BillExemptionRule.class, + supportedOpenmrsVersions = {"2.0 - 2.*"}) +public class BillExemptionRuleResource extends DelegatingSubResource { + + @Override + public BillExemptionRule newDelegate() { + return new BillExemptionRule(); + } + + @Override + public BillExemptionRule save(BillExemptionRule delegate) { + BillExemption exemption = delegate.getBillingExemption(); + if (exemption != null) { + getService().save(exemption); + } + return delegate; + } + + @Override + public BillExemption getParent(BillExemptionRule instance) { + return instance.getBillingExemption(); + } + + @Override + public void setParent(BillExemptionRule instance, BillExemption parent) { + instance.setBillingExemption(parent); + } + + @Override + public PageableResult doGetAll(BillExemption parent, RequestContext context) throws ResponseException { + List rules = parent.getRules(); + if (rules == null) { + rules = new ArrayList<>(); + } + return new NeedsPaging<>(rules, context); + } + + @Override + public BillExemptionRule getByUniqueId(String uniqueId) { + throw new ResourceDoesNotSupportOperationException("BillingExemptionRule does not support lookup by UUID"); + } + + @Override + protected void delete(BillExemptionRule delegate, String reason, RequestContext context) throws ResponseException { + if (delegate.getVoided()) { + return; + } + delegate.setVoided(true); + delegate.setVoidReason(reason); + BillExemption exemption = delegate.getBillingExemption(); + if (exemption != null) { + getService().save(exemption); + } + } + + @Override + public void purge(BillExemptionRule delegate, RequestContext context) throws ResponseException { + BillExemption exemption = delegate.getBillingExemption(); + if (exemption != null) { + exemption.getRules().remove(delegate); + getService().save(exemption); + } + } + + @Override + public DelegatingResourceDescription getRepresentationDescription(Representation rep) { + DelegatingResourceDescription description = new DelegatingResourceDescription(); + + if (rep instanceof RefRepresentation) { + description.addProperty("uuid"); + description.addProperty("scriptType"); + description.addProperty("script"); + } else if (rep instanceof DefaultRepresentation) { + description.addProperty("uuid"); + description.addProperty("scriptType"); + description.addProperty("script"); + description.addProperty("voided"); + } else if (rep instanceof FullRepresentation) { + description.addProperty("uuid"); + description.addProperty("scriptType"); + description.addProperty("script"); + description.addProperty("voided"); + description.addProperty("voidReason"); + description.addProperty("auditInfo"); + } + + return description; + } + + @Override + public DelegatingResourceDescription getCreatableProperties() { + DelegatingResourceDescription description = new DelegatingResourceDescription(); + description.addProperty("scriptType"); + description.addProperty("script"); + return description; + } + + @Override + public DelegatingResourceDescription getUpdatableProperties() { + return getCreatableProperties(); + } + + @PropertySetter("scriptType") + public void setScriptType(BillExemptionRule instance, String scriptType) { + if (scriptType != null) { + instance.setScriptType(ScriptType.valueOf(scriptType)); + } + } + + @PropertySetter("script") + public void setScript(BillExemptionRule instance, String script) { + if (script != null) { + instance.setScript(StringEscapeUtils.unescapeHtml(script)); + } + } + + private BillExemptionService getService() { + return Context.getService(BillExemptionService.class); + } +} \ No newline at end of file diff --git a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillLineItemResource.java b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillLineItemResource.java index 4d357c63..42f4e73d 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillLineItemResource.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillLineItemResource.java @@ -59,8 +59,9 @@ public DelegatingResourceDescription getRepresentationDescription(Representation description.addProperty("priceUuid"); description.addProperty("lineItemOrder"); description.addProperty("paymentStatus"); + return description; } - return description; + return null; } @PropertySetter(value = "item") diff --git a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillResource.java b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillResource.java index c0b89f39..b25990b7 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillResource.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillResource.java @@ -14,23 +14,23 @@ package org.openmrs.module.billing.web.rest.resource; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.stream.Collectors; -import org.apache.logging.log4j.util.Strings; -import org.openmrs.Patient; +import org.apache.commons.lang3.StringUtils; import org.openmrs.Provider; import org.openmrs.User; import org.openmrs.api.AdministrationService; import org.openmrs.api.ProviderService; import org.openmrs.api.context.Context; import org.openmrs.module.billing.ModuleSettings; -import org.openmrs.module.billing.api.IBillService; -import org.openmrs.module.billing.api.ICashPointService; +import org.openmrs.module.billing.api.BillService; import org.openmrs.module.billing.api.ITimesheetService; -import org.openmrs.module.billing.api.base.entity.IEntityDataService; +import org.openmrs.module.billing.api.base.PagingInfo; import org.openmrs.module.billing.api.model.Bill; import org.openmrs.module.billing.api.model.BillLineItem; import org.openmrs.module.billing.api.model.BillStatus; @@ -40,16 +40,21 @@ import org.openmrs.module.billing.api.search.BillSearch; import org.openmrs.module.billing.api.util.RoundingUtil; import org.openmrs.module.billing.web.base.resource.BaseRestDataResource; +import org.openmrs.module.billing.web.base.resource.PagingUtil; import org.openmrs.module.billing.web.rest.controller.base.CashierResourceController; import org.openmrs.module.webservices.rest.web.RequestContext; import org.openmrs.module.webservices.rest.web.RestConstants; import org.openmrs.module.webservices.rest.web.annotation.PropertySetter; import org.openmrs.module.webservices.rest.web.annotation.Resource; import org.openmrs.module.webservices.rest.web.representation.DefaultRepresentation; -import org.openmrs.module.webservices.rest.web.representation.RefRepresentation; +import org.openmrs.module.webservices.rest.web.representation.FullRepresentation; import org.openmrs.module.webservices.rest.web.representation.Representation; +import org.openmrs.module.webservices.rest.web.resource.api.PageableResult; import org.openmrs.module.webservices.rest.web.resource.impl.AlreadyPaged; +import org.openmrs.module.webservices.rest.web.resource.impl.DataDelegatingCrudResource; import org.openmrs.module.webservices.rest.web.resource.impl.DelegatingResourceDescription; +import org.openmrs.module.webservices.rest.web.resource.impl.NeedsPaging; +import org.openmrs.module.webservices.rest.web.response.ResponseException; import org.springframework.web.client.RestClientException; /** @@ -57,11 +62,11 @@ */ @Resource(name = RestConstants.VERSION_1 + CashierResourceController.BILLING_NAMESPACE + "/bill", supportedClass = Bill.class, supportedOpenmrsVersions = {"2.0 - 2.*"}) -public class BillResource extends BaseRestDataResource { +public class BillResource extends DataDelegatingCrudResource { @Override public DelegatingResourceDescription getRepresentationDescription(Representation rep) { - DelegatingResourceDescription description = super.getRepresentationDescription(rep); - if (!(rep instanceof RefRepresentation)) { + if (rep instanceof DefaultRepresentation || rep instanceof FullRepresentation) { + DelegatingResourceDescription description = new DelegatingResourceDescription(); description.addProperty("adjustedBy", Representation.REF); description.addProperty("billAdjusted", Representation.REF); description.addProperty("cashPoint", Representation.REF); @@ -74,8 +79,9 @@ public DelegatingResourceDescription getRepresentationDescription(Representation description.addProperty("status"); description.addProperty("adjustmentReason"); description.addProperty("id"); + return description; } - return description; + return null; } @Override @@ -86,7 +92,7 @@ public DelegatingResourceDescription getCreatableProperties() { @PropertySetter("lineItems") public void setBillLineItems(Bill instance, List lineItems) { if (instance.getLineItems() == null) { - instance.setLineItems(new ArrayList(lineItems.size())); + instance.setLineItems(new ArrayList<>(lineItems.size())); } BaseRestDataResource.syncCollection(instance.getLineItems(), lineItems); for (BillLineItem item : instance.getLineItems()) { @@ -136,7 +142,7 @@ public Bill save(Bill bill) { if (bill.getId() == null) { if (bill.getCashier() == null) { - Provider cashier = getCurrentCashier(bill); + Provider cashier = getCurrentCashier(); if (cashier == null) { throw new RestClientException("Couldn't find Provider for the current user (" + Context.getAuthenticatedUser().getUsername() + ")"); @@ -149,7 +155,7 @@ public Bill save(Bill bill) { loadBillCashPoint(bill); } - // Now that all all attributes have been set (i.e., payments and bill status) we can check to see if the bill + // Now that all attributes have been set (i.e., payments and bill status) we can check to see if the bill // is fully paid. bill.synchronizeBillStatus(); if (bill.getStatus() == null) { @@ -157,45 +163,51 @@ public Bill save(Bill bill) { } } - return super.save(bill); + return Context.getService(BillService.class).saveBill(bill); } @Override protected AlreadyPaged doSearch(RequestContext context) { - String patientUuid = context.getRequest().getParameter("patientUuid"); - String status = context.getRequest().getParameter("status"); - String cashPointUuid = context.getRequest().getParameter("cashPointUuid"); + BillSearch billSearch = buildBillSearchFromRequest(context); + PagingInfo pagingInfo = PagingUtil.getPagingInfoFromContext(context); - Patient patient = Strings.isNotEmpty(patientUuid) ? Context.getPatientService().getPatientByUuid(patientUuid) : null; - BillStatus billStatus = Strings.isNotEmpty(status) ? BillStatus.valueOf(status.toUpperCase()) : null; - CashPoint cashPoint = Strings.isNotEmpty(cashPointUuid) ? Context.getService(ICashPointService.class).getByUuid(cashPointUuid) : null; + BillService service = Context.getService(BillService.class); + List result = service.getBills(billSearch, pagingInfo); - Bill searchTemplate = new Bill(); - searchTemplate.setPatient(patient); - searchTemplate.setStatus(billStatus); - searchTemplate.setCashPoint(cashPoint); - IBillService service = Context.getService(IBillService.class); - - List result = service.getBills(new BillSearch(searchTemplate, false)); - return new AlreadyPaged<>(context, result, false); + return new AlreadyPaged<>(context, result, pagingInfo.hasMoreResults(), pagingInfo.getTotalRecordCount()); } - @SuppressWarnings("unchecked") + + /** + * Gets a bill by UUID + * + * @param uniqueId The bill UUID. + * @return The bill with the specified UUID without voided line items. + */ @Override - public Class> getServiceClass() { - return (Class>) (Object) IBillService.class; + public Bill getByUniqueId(String uniqueId) { + if (StringUtils.isBlank(uniqueId)) { + return null; + } + + return Context.getService(BillService.class).getBillByUuid(uniqueId); } - public String getDisplayString(Bill instance) { - return instance.getReceiptNumber(); + @Override + protected void delete(Bill bill, String s, RequestContext requestContext) throws ResponseException { + Context.getService(BillService.class).voidBill(bill, s); } + @Override + public void purge(Bill bill, RequestContext requestContext) throws ResponseException { + Context.getService(BillService.class).purgeBill(bill); + } @Override public Bill newDelegate() { return new Bill(); } - private Provider getCurrentCashier(Bill bill) { + private Provider getCurrentCashier() { User currentUser = Context.getAuthenticatedUser(); ProviderService service = Context.getProviderService(); Collection providers = service.getProvidersByPerson(currentUser.getPerson()); @@ -234,4 +246,41 @@ private void loadBillCashPoint(Bill bill) { bill.setCashPoint(cashPoint); } } + + + private BillSearch buildBillSearchFromRequest(RequestContext context) { + BillSearch billSearch = new BillSearch(); + + String patientUuid = context.getRequest().getParameter("patientUuid"); + if (StringUtils.isNotBlank(patientUuid)) { + billSearch.setPatientUuid(patientUuid); + } + + String patientName = context.getRequest().getParameter("patientName"); + if (StringUtils.isNotBlank(patientName)) { + billSearch.setPatientName(patientName); + } + + String status = context.getRequest().getParameter("status"); + if (StringUtils.isNotBlank(status)) { + List statuses = Arrays.stream(status.split(",")) + .map(String::trim) + .filter(StringUtils::isNotBlank) + .map(s -> BillStatus.valueOf(s.toUpperCase())) + .collect(Collectors.toList()); + billSearch.setStatuses(statuses); + } + + String cashPointUuid = context.getRequest().getParameter("cashPointUuid"); + if (StringUtils.isNotBlank(cashPointUuid)) { + billSearch.setCashPointUuid(cashPointUuid); + } + + String includeAll = context.getRequest().getParameter("includeAll"); + if (StringUtils.isNotBlank(includeAll)) { + billSearch.setIncludeVoidedLineItems(Boolean.parseBoolean(includeAll)); + } + + return billSearch; + } } diff --git a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillableServiceResource.java b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillableServiceResource.java index 7ae91672..4a77564d 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillableServiceResource.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillableServiceResource.java @@ -16,11 +16,14 @@ import org.apache.logging.log4j.util.Strings; import org.openmrs.Concept; import org.openmrs.api.context.Context; +import org.openmrs.module.billing.api.base.entity.IMetadataDataService; +import org.openmrs.module.billing.api.model.BillableService; +import org.openmrs.module.billing.api.model.BillableServiceStatus; +import org.openmrs.module.billing.api.model.CashierItemPrice; import org.openmrs.module.billing.web.base.resource.BaseRestDataResource; +import org.openmrs.module.billing.web.base.resource.BaseRestMetadataResource; import org.openmrs.module.billing.web.rest.controller.base.CashierResourceController; import org.openmrs.module.billing.api.IBillableItemsService; -import org.openmrs.module.billing.api.base.entity.IEntityDataService; -import org.openmrs.module.billing.api.model.*; import org.openmrs.module.billing.api.search.BillableServiceSearch; import org.openmrs.module.webservices.rest.web.RequestContext; import org.openmrs.module.webservices.rest.web.RestConstants; @@ -40,7 +43,7 @@ @Resource(name = RestConstants.VERSION_1 + CashierResourceController.BILLING_NAMESPACE + "/billableService", supportedClass = BillableService.class, supportedOpenmrsVersions = {"2.0 - 2.*"}) -public class BillableServiceResource extends BaseRestDataResource { +public class BillableServiceResource extends BaseRestMetadataResource { @Override public BillableService newDelegate() { @@ -48,7 +51,7 @@ public BillableService newDelegate() { } @Override - public Class> getServiceClass() { + public Class> getServiceClass() { return IBillableItemsService.class; } diff --git a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/CashierItemPriceResource.java b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/CashierItemPriceResource.java index e4943b51..a4e6e850 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/CashierItemPriceResource.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/CashierItemPriceResource.java @@ -14,10 +14,10 @@ package org.openmrs.module.billing.web.rest.resource; import org.openmrs.api.context.Context; -import org.openmrs.module.billing.web.base.resource.BaseRestDataResource; +import org.openmrs.module.billing.api.base.entity.IMetadataDataService; +import org.openmrs.module.billing.web.base.resource.BaseRestMetadataResource; import org.openmrs.module.billing.web.rest.controller.base.CashierResourceController; import org.openmrs.module.billing.api.ICashierItemPriceService; -import org.openmrs.module.billing.api.base.entity.IEntityDataService; import org.openmrs.module.billing.api.model.CashierItemPrice; import org.openmrs.module.stockmanagement.api.StockManagementService; import org.openmrs.module.stockmanagement.api.model.StockItem; @@ -36,14 +36,14 @@ @Resource(name = RestConstants.VERSION_1 + CashierResourceController.BILLING_NAMESPACE + "/cashierItemPrice", supportedClass = CashierItemPrice.class, supportedOpenmrsVersions = {"2.0 - 2.*"}) -public class CashierItemPriceResource extends BaseRestDataResource { +public class CashierItemPriceResource extends BaseRestMetadataResource { @Override public CashierItemPrice newDelegate() { return new CashierItemPrice(); } @Override - public Class> getServiceClass() { + public Class> getServiceClass() { return ICashierItemPriceService.class; } diff --git a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/PaymentResource.java b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/PaymentResource.java index b29958fd..38568608 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/PaymentResource.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/PaymentResource.java @@ -14,8 +14,8 @@ package org.openmrs.module.billing.web.rest.resource; import org.openmrs.api.context.Context; +import org.openmrs.module.billing.api.BillService; import org.openmrs.module.billing.web.base.resource.BaseRestDataResource; -import org.openmrs.module.billing.api.IBillService; import org.openmrs.module.billing.api.IPaymentModeService; import org.openmrs.module.billing.api.model.Bill; import org.openmrs.module.billing.api.model.Payment; @@ -48,18 +48,18 @@ public class PaymentResource extends DelegatingSubResourceproperty = 'cashier.receipt.logoPath' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Create tables for billing exemptions and exemption rules + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml index 9f552ecc..a442aba8 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.openmrs.module billing - 1.3.3-SNAPSHOT + 2.0.0-SNAPSHOT pom OpenMRS Billing Module Module to provide basic billing functionality @@ -48,14 +48,14 @@ - 2.4.0 + 2.7.8-SNAPSHOT UTF-8 2.0.9 1.8 - 2.4.0 + 2.4.0 8.0.2 1.4.0 - 2.0.0 + 2.4.0 1.18.38 @@ -63,7 +63,6 @@ - org.openmrs.api openmrs-api @@ -114,13 +113,13 @@ org.openmrs.module webservices.rest-omod - 2.9 + 2.49.0 provided org.openmrs.module webservices.rest-omod-common - 2.9 + 2.49.0 provided @@ -148,6 +147,27 @@ provided + + org.openmrs.module + fhir2-api-2.5 + ${fhir2Version} + provided + + + + org.openmrs.module + fhir2-api-2.6 + ${fhir2Version} + provided + + + + org.openmrs.module + fhir2-api-2.7 + ${fhir2Version} + provided + + org.openmrs.module fhir2-api @@ -324,6 +344,12 @@ provided + + org.mockito + mockito-inline + 3.12.4 + test + From bc2e6eb88a04f195fa187932a308438b67a55427 Mon Sep 17 00:00:00 2001 From: Mutagubya Jonathan Date: Fri, 12 Dec 2025 15:30:16 +0300 Subject: [PATCH 05/20] (feat) Disable Bill auto creation on Drug Orders (#5) From 5d150fc032239dacab20705325806d96f9181242 Mon Sep 17 00:00:00 2001 From: Mutagubya Jonathan Date: Mon, 15 Dec 2025 12:03:05 +0300 Subject: [PATCH 06/20] Revert "(feat) Disable Bill auto creation on Drug Orders (#5)" (#6) This reverts commit bc2e6eb88a04f195fa187932a308438b67a55427. From 095f0a37b05239751711a538f25614c73164e84f Mon Sep 17 00:00:00 2001 From: Mutagubya Jonathan Date: Mon, 15 Dec 2025 12:03:49 +0300 Subject: [PATCH 07/20] Revert "Merge remote-tracking branch upstream/main (#4)" (#7) This reverts commit 1852b2fb30a074daa4039cb4e556a6d961f02f29. --- .github/workflows/maven.yml | 2 +- api/pom.xml | 14 +- .../advice/GenerateBillFromOrderAdvice.java | 105 ++-- .../OrderCreationMethodBeforeAdvice.java | 7 +- .../billing/api/BillExemptionService.java | 20 - .../module/billing/api/BillService.java | 152 ----- .../module/billing/api/IBillService.java | 119 ++++ .../billing/api/IBillableItemsService.java | 4 +- .../billing/api/ICashierItemPriceService.java | 4 +- .../module/billing/api/ItemPriceService.java | 6 +- .../module/billing/api/base/PagingInfo.java | 42 +- .../api/base/entity/IObjectDataService.java | 2 +- .../impl/BaseEntityDataServiceImpl.java | 4 +- .../impl/BaseMetadataDataServiceImpl.java | 4 +- .../impl/BaseObjectDataServiceImpl.java | 4 +- .../entity/model/BaseCustomizableData.java | 2 +- .../module/billing/api/db/BillDAO.java | 110 ---- .../billing/api/db/BillExemptionDAO.java | 24 - .../db/hibernate/BillExemptionDAOImpl.java | 94 --- .../db/hibernate/HibernateBillDAOImpl.java | 191 ------ .../api/evaluator/ExemptionEvaluator.java | 11 - .../api/evaluator/ExemptionRuleEngine.java | 35 -- .../billing/api/evaluator/ScriptType.java | 16 - .../evaluator/impl/JSExemptionEvaluator.java | 67 -- .../api/impl/BillExemptionServiceImpl.java | 48 -- .../api/impl/BillLineItemServiceImpl.java | 48 +- .../billing/api/impl/BillServiceImpl.java | 585 +++++++++++++++--- .../api/impl/BillableItemsServiceImpl.java | 10 +- .../impl/ICashierItemPriceServiceImpl.java | 10 +- .../api/impl/ItemPriceServiceImpl.java | 14 +- .../module/billing/api/model/Bill.java | 180 +++++- .../billing/api/model/BillExemption.java | 82 --- .../billing/api/model/BillExemptionRule.java | 78 --- .../billing/api/model/BillLineItem.java | 7 +- .../billing/api/model/BillableService.java | 4 +- .../billing/api/model/CashierItemPrice.java | 4 +- .../billing/api/model/ExemptionType.java | 7 - .../module/billing/api/search/BillSearch.java | 60 +- .../api/search/BillableServiceSearch.java | 4 +- .../billing/api/util/PrivilegeConstants.java | 2 - .../exemptions/BillingExemptionChecker.java | 26 + .../billing/exemptions/BillingExemptions.java | 50 ++ .../exemptions/BillingExemptionsConfig.java | 28 + .../exemptions/DefaultBillingExemptions.java | 32 + .../SampleBillingExemptionBuilder.java | 126 ++++ .../exemptions/SampleBillingExemptions.json | 50 ++ .../module/billing/util/ReceiptGenerator.java | 285 --------- .../openmrs/module/billing/util/Utils.java | 4 + .../billing/validator/BillValidator.java | 44 -- api/src/main/resources/Bill.hbm.xml | 20 +- .../resources/moduleApplicationContext.xml | 81 +-- .../module/billing/IBillServiceTest.java | 464 ++++++++++++++ .../module/billing/ICashPointServiceTest.java | 4 +- .../module/billing/ITimesheetServiceTest.java | 4 +- .../SequentialReceiptNumberGeneratorTest.java | 64 +- .../openmrs/module/billing/TestConstants.java | 2 - .../hibernate/BillExemptionDAOImplTest.java | 296 --------- .../evaluator/ExemptionRuleEngineTest.java | 289 --------- .../impl/JSExemptionEvaluatorTest.java | 119 ---- .../impl/BillExemptionServiceImplTest.java | 137 ---- .../module/billing/api/model/BillTest.java | 195 ------ .../base/entity/IObjectDataServiceTest.java | 22 +- .../billing/db/HibernateBillDAOImplTest.java | 243 -------- .../billing/impl/BillServiceImplTest.java | 508 ++++----------- .../impl/CashPointServiceImplTest.java | 190 ------ .../impl/CashierOptionsServiceGpImplTest.java | 388 ++++++------ .../billing/validator/BillValidatorTest.java | 77 --- .../billing/api/include/BillExemptionTest.xml | 107 ---- .../module/billing/api/include/BillTest.xml | 57 +- .../billing/api/include/CoreTest-2.0.xml | 3 - .../api/include/StockOperationType.xml | 41 -- fhir/pom.xml | 18 +- .../impl/FhirInvoiceServiceImplTest.java | 1 + lombok.config | 3 - omod/pom.xml | 2 +- .../base/resource/BaseRestDataResource.java | 9 +- .../resource/BaseRestMetadataResource.java | 2 +- .../base/resource/BaseRestObjectResource.java | 2 +- ...tractSequentialReceiptNumberGenerator.java | 2 +- .../controller/BillAddEditController.java | 9 +- .../controller/CashierController.java | 2 +- .../PatientBillHistoryController.java | 10 +- .../legacyweb/filter/CashierLogoutFilter.java | 2 +- .../controller/CashierRestController.java | 2 +- .../rest/controller/ReceiptController.java | 17 +- .../rest/resource/BillExemptionResource.java | 142 ----- .../resource/BillExemptionRuleResource.java | 161 ----- .../rest/resource/BillLineItemResource.java | 3 +- .../web/rest/resource/BillResource.java | 117 +--- .../resource/BillableServiceResource.java | 11 +- .../resource/CashierItemPriceResource.java | 8 +- .../web/rest/resource/PaymentResource.java | 24 +- omod/src/main/resources/liquibase.xml | 150 ----- pom.xml | 40 +- 94 files changed, 2217 insertions(+), 4658 deletions(-) delete mode 100644 api/src/main/java/org/openmrs/module/billing/api/BillExemptionService.java delete mode 100644 api/src/main/java/org/openmrs/module/billing/api/BillService.java create mode 100644 api/src/main/java/org/openmrs/module/billing/api/IBillService.java delete mode 100644 api/src/main/java/org/openmrs/module/billing/api/db/BillDAO.java delete mode 100644 api/src/main/java/org/openmrs/module/billing/api/db/BillExemptionDAO.java delete mode 100644 api/src/main/java/org/openmrs/module/billing/api/db/hibernate/BillExemptionDAOImpl.java delete mode 100644 api/src/main/java/org/openmrs/module/billing/api/db/hibernate/HibernateBillDAOImpl.java delete mode 100644 api/src/main/java/org/openmrs/module/billing/api/evaluator/ExemptionEvaluator.java delete mode 100644 api/src/main/java/org/openmrs/module/billing/api/evaluator/ExemptionRuleEngine.java delete mode 100644 api/src/main/java/org/openmrs/module/billing/api/evaluator/ScriptType.java delete mode 100644 api/src/main/java/org/openmrs/module/billing/api/evaluator/impl/JSExemptionEvaluator.java delete mode 100644 api/src/main/java/org/openmrs/module/billing/api/impl/BillExemptionServiceImpl.java delete mode 100644 api/src/main/java/org/openmrs/module/billing/api/model/BillExemption.java delete mode 100644 api/src/main/java/org/openmrs/module/billing/api/model/BillExemptionRule.java delete mode 100644 api/src/main/java/org/openmrs/module/billing/api/model/ExemptionType.java create mode 100644 api/src/main/java/org/openmrs/module/billing/exemptions/BillingExemptionChecker.java create mode 100644 api/src/main/java/org/openmrs/module/billing/exemptions/BillingExemptions.java create mode 100644 api/src/main/java/org/openmrs/module/billing/exemptions/BillingExemptionsConfig.java create mode 100644 api/src/main/java/org/openmrs/module/billing/exemptions/DefaultBillingExemptions.java create mode 100644 api/src/main/java/org/openmrs/module/billing/exemptions/SampleBillingExemptionBuilder.java create mode 100644 api/src/main/java/org/openmrs/module/billing/exemptions/SampleBillingExemptions.json delete mode 100644 api/src/main/java/org/openmrs/module/billing/util/ReceiptGenerator.java delete mode 100644 api/src/main/java/org/openmrs/module/billing/validator/BillValidator.java create mode 100644 api/src/test/java/org/openmrs/module/billing/IBillServiceTest.java delete mode 100644 api/src/test/java/org/openmrs/module/billing/api/db/hibernate/BillExemptionDAOImplTest.java delete mode 100644 api/src/test/java/org/openmrs/module/billing/api/evaluator/ExemptionRuleEngineTest.java delete mode 100644 api/src/test/java/org/openmrs/module/billing/api/evaluator/impl/JSExemptionEvaluatorTest.java delete mode 100644 api/src/test/java/org/openmrs/module/billing/api/impl/BillExemptionServiceImplTest.java delete mode 100644 api/src/test/java/org/openmrs/module/billing/api/model/BillTest.java delete mode 100644 api/src/test/java/org/openmrs/module/billing/db/HibernateBillDAOImplTest.java delete mode 100644 api/src/test/java/org/openmrs/module/billing/impl/CashPointServiceImplTest.java delete mode 100644 api/src/test/java/org/openmrs/module/billing/validator/BillValidatorTest.java delete mode 100644 api/src/test/resources/org/openmrs/module/billing/api/include/BillExemptionTest.xml delete mode 100644 api/src/test/resources/org/openmrs/module/billing/api/include/StockOperationType.xml delete mode 100644 lombok.config delete mode 100644 omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillExemptionResource.java delete mode 100644 omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillExemptionRuleResource.java diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 94817daa..74ee1259 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: platform: [ ubuntu-latest ] - java-version: [ 8, 11, 17, 21 ] + java-version: [ 8 ] runs-on: ${{ matrix.platform }} env: diff --git a/api/pom.xml b/api/pom.xml index 80f2aed5..161e308e 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -4,7 +4,7 @@ org.openmrs.module billing - 2.0.0-SNAPSHOT + 1.3.3-SNAPSHOT billing-api @@ -87,17 +87,7 @@ com.itextpdf font-asian - - - org.mockito - mockito-inline - test - - - org.projectlombok - lombok - - + diff --git a/api/src/main/java/org/openmrs/module/billing/advice/GenerateBillFromOrderAdvice.java b/api/src/main/java/org/openmrs/module/billing/advice/GenerateBillFromOrderAdvice.java index 5f8f34fb..597771cb 100644 --- a/api/src/main/java/org/openmrs/module/billing/advice/GenerateBillFromOrderAdvice.java +++ b/api/src/main/java/org/openmrs/module/billing/advice/GenerateBillFromOrderAdvice.java @@ -12,22 +12,20 @@ import org.openmrs.api.OrderService; import org.openmrs.api.ProgramWorkflowService; import org.openmrs.api.context.Context; -import org.openmrs.module.billing.api.BillExemptionService; -import org.openmrs.module.billing.api.BillService; +import org.openmrs.module.billing.api.IBillService; import org.openmrs.module.billing.api.IBillableItemsService; import org.openmrs.module.billing.api.ICashPointService; import org.openmrs.module.billing.api.ItemPriceService; -import org.openmrs.module.billing.api.evaluator.ExemptionRuleEngine; import org.openmrs.module.billing.api.model.Bill; -import org.openmrs.module.billing.api.model.BillExemption; import org.openmrs.module.billing.api.model.BillLineItem; import org.openmrs.module.billing.api.model.BillStatus; import org.openmrs.module.billing.api.model.BillableService; import org.openmrs.module.billing.api.model.BillableServiceStatus; import org.openmrs.module.billing.api.model.CashPoint; import org.openmrs.module.billing.api.model.CashierItemPrice; -import org.openmrs.module.billing.api.model.ExemptionType; import org.openmrs.module.billing.api.search.BillableServiceSearch; +import org.openmrs.module.billing.exemptions.BillingExemptions; +import org.openmrs.module.billing.util.Utils; import org.openmrs.module.stockmanagement.api.StockManagementService; import org.openmrs.module.stockmanagement.api.model.StockItem; import org.springframework.aop.AfterReturningAdvice; @@ -36,9 +34,10 @@ import java.math.BigDecimal; import java.util.ArrayList; import java.util.Date; -import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; public class GenerateBillFromOrderAdvice implements AfterReturningAdvice { @@ -49,7 +48,7 @@ public class GenerateBillFromOrderAdvice implements AfterReturningAdvice { OrderService orderService = Context.getOrderService(); - BillService billService = Context.getService(BillService.class); + IBillService billService = Context.getService(IBillService.class); StockManagementService stockService = Context.getService(StockManagementService.class); @@ -57,10 +56,6 @@ public class GenerateBillFromOrderAdvice implements AfterReturningAdvice { ICashPointService cashPointService = Context.getService(ICashPointService.class); - ExemptionRuleEngine exemptionRuleEngine = Context.getRegisteredComponent("ruleEngine", ExemptionRuleEngine.class); - - BillExemptionService billExemptionService = Context.getService(BillExemptionService.class); - /** * This is called immediately an order is saved */ @@ -95,7 +90,7 @@ public void afterReturning(Object returnValue, Method method, Object[] args, Obj if (!stockItems.isEmpty()) { // check from the list for all exemptions - boolean isExempted = checkIfOrderIsExempted(workflowService, order, ExemptionType.COMMODITY); + boolean isExempted = checkIfOrderIsExempted(workflowService, order, BillingExemptions.COMMODITIES); BillStatus lineItemStatus = isExempted ? BillStatus.EXEMPTED : BillStatus.PENDING; addBillItemToBill(order, patient, cashierUUID, stockItems.get(0), null, (int) drugQuantity, order.getDateActivated(), lineItemStatus); @@ -109,7 +104,7 @@ public void afterReturning(Object returnValue, Method method, Object[] args, Obj IBillableItemsService service = Context.getService(IBillableItemsService.class); List searchResult = service.findServices(new BillableServiceSearch(searchTemplate)); if (!searchResult.isEmpty()) { - boolean isExempted = checkIfOrderIsExempted(workflowService, order, ExemptionType.SERVICE); + boolean isExempted = checkIfOrderIsExempted(workflowService, order, BillingExemptions.SERVICES); BillStatus lineItemStatus = isExempted ? BillStatus.EXEMPTED : BillStatus.PENDING; addBillItemToBill(order, patient, cashierUUID, null, searchResult.get(0), 1, order.getDateActivated(), lineItemStatus); @@ -122,54 +117,56 @@ public void afterReturning(Object returnValue, Method method, Object[] args, Obj } } + /** + * Checks if an order concept is in the exemptions list + * + * @param workflowService + * @param order + * @param config + * @return + */ private boolean checkIfOrderIsExempted(ProgramWorkflowService workflowService, Order order, - ExemptionType exemptionType) { - if (order == null || order.getConcept() == null) { + Map> config) { + if (config == null || order == null || config.size() == 0) { return false; } - List exemptions = billExemptionService.getExemptionsByConcept(order.getConcept(), exemptionType, - false); - - if (exemptions == null || exemptions.isEmpty()) { - return false; + if (config.get("all") != null && config.get("all").contains(order.getConcept().getConceptId())) { + return true; } - - Map variables = buildVariablesMap(order, workflowService); - - for (BillExemption exemption : exemptions) { - if (exemptionRuleEngine.isExemptionApplicable(exemption, variables)) { - return true; + // check in programs list + List programExemptions = config.keySet().stream().filter(key -> key.startsWith("program:")) + .collect(Collectors.toList()); + if (programExemptions.size() > 0) { + List programs = workflowService.getPatientPrograms(order.getPatient(), null, null, null, + new Date(), null, false); + Set activeEnrollments = new HashSet<>(); + programs.forEach(patientProgram -> { + if (patientProgram.getActive()) { + activeEnrollments.add(patientProgram.getProgram().getName()); + } + }); + + for (String programEntry : programExemptions) { + if (programEntry.contains(":")) { // this is our convention to distinguish program exemption + String programName = programEntry.substring(programEntry.indexOf(":") + 1); + //check if patient is active in the program + if (activeEnrollments.contains(programName)) { + // check if order is exempted + if (config.get(programEntry).contains(order.getConcept().getConceptId())) { + return true; + } + + } + } } } - return false; - } - - private Map buildVariablesMap(Order order, ProgramWorkflowService workflowService) { - Map variables = new HashMap<>(); - - Patient patient = order.getPatient(); - variables.put("patient", patient); - // We cannot call getAge() method from Java Script - if (patient != null) { - variables.put("patientAge", patient.getAge()); + // check age category + if (order.getPatient().getAge() < 5 && config.get("age<5") != null + && config.get("age<5").contains(order.getConcept().getConceptId())) { + return true; } - - Map orderData = new HashMap<>(); - orderData.put("uuid", order.getUuid()); - if (order.getConcept() != null) { - orderData.put("conceptId", order.getConcept().getConceptId()); - } - variables.put("order", orderData); - - List programs = workflowService.getPatientPrograms(patient, null, null, null, new Date(), null, - false); - List activePrograms = programs.stream().filter(PatientProgram::getActive) - .map(pp -> pp.getProgram().getName()).collect(Collectors.toList()); - - variables.put("activePrograms", activePrograms); - - return variables; + return false; } /** @@ -221,7 +218,7 @@ public void addBillItemToBill(Order order, Patient patient, String cashierUUID, activeBill.setCashPoint(cashPoints.get(0)); activeBill.addLineItem(billLineItem); activeBill.setStatus(BillStatus.PENDING); - billService.saveBill(activeBill); + billService.save(activeBill); } else { LOG.error("User is not a provider"); } diff --git a/api/src/main/java/org/openmrs/module/billing/advice/OrderCreationMethodBeforeAdvice.java b/api/src/main/java/org/openmrs/module/billing/advice/OrderCreationMethodBeforeAdvice.java index c46f0a97..4030df12 100644 --- a/api/src/main/java/org/openmrs/module/billing/advice/OrderCreationMethodBeforeAdvice.java +++ b/api/src/main/java/org/openmrs/module/billing/advice/OrderCreationMethodBeforeAdvice.java @@ -32,7 +32,7 @@ import org.openmrs.VisitAttribute; import org.openmrs.api.OrderService; import org.openmrs.api.context.Context; -import org.openmrs.module.billing.api.BillService; +import org.openmrs.module.billing.api.IBillService; import org.openmrs.module.billing.api.IBillableItemsService; import org.openmrs.module.billing.api.ICashPointService; import org.openmrs.module.billing.api.ItemPriceService; @@ -44,6 +44,7 @@ import org.openmrs.module.billing.api.model.CashPoint; import org.openmrs.module.billing.api.model.CashierItemPrice; import org.openmrs.module.billing.api.search.BillableServiceSearch; +import org.openmrs.module.billing.util.Utils; import org.openmrs.module.stockmanagement.api.StockManagementService; import org.openmrs.module.stockmanagement.api.model.StockItem; import org.springframework.aop.MethodBeforeAdvice; @@ -56,7 +57,7 @@ public class OrderCreationMethodBeforeAdvice implements MethodBeforeAdvice { OrderService orderService = Context.getOrderService(); - BillService billService = Context.getService(BillService.class); + IBillService billService = Context.getService(IBillService.class); StockManagementService stockService = Context.getService(StockManagementService.class); @@ -165,7 +166,7 @@ public void addBillItemToBill(Order order, Patient patient, String cashierUUID, activeBill.setCashPoint(cashPoints.get(0)); activeBill.addLineItem(billLineItem); activeBill.setStatus(BillStatus.PENDING); - billService.saveBill(activeBill); + billService.save(activeBill); } } diff --git a/api/src/main/java/org/openmrs/module/billing/api/BillExemptionService.java b/api/src/main/java/org/openmrs/module/billing/api/BillExemptionService.java deleted file mode 100644 index f806c35c..00000000 --- a/api/src/main/java/org/openmrs/module/billing/api/BillExemptionService.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.openmrs.module.billing.api; - -import org.openmrs.Concept; -import org.openmrs.module.billing.api.model.BillExemption; -import org.openmrs.module.billing.api.model.ExemptionType; - -import java.util.List; - -public interface BillExemptionService { - - BillExemption save(BillExemption billExemption); - - BillExemption getBillingExemptionById(Integer id); - - BillExemption getBillingExemptionByUuid(String uuid); - - List getExemptionsByConcept(Concept concept, ExemptionType itemType, boolean includeRetired); - - List getExemptionsByItemType(ExemptionType itemType, boolean includeRetired); -} diff --git a/api/src/main/java/org/openmrs/module/billing/api/BillService.java b/api/src/main/java/org/openmrs/module/billing/api/BillService.java deleted file mode 100644 index cdfe0a87..00000000 --- a/api/src/main/java/org/openmrs/module/billing/api/BillService.java +++ /dev/null @@ -1,152 +0,0 @@ -package org.openmrs.module.billing.api; - -import org.openmrs.annotation.Authorized; -import org.openmrs.api.OpenmrsService; -import org.openmrs.module.billing.api.base.PagingInfo; -import org.openmrs.module.billing.api.model.Bill; -import org.openmrs.module.billing.api.search.BillSearch; -import org.openmrs.module.billing.api.util.PrivilegeConstants; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; - -/** - * Service interface for managing billing operations - * - * @see Bill - * @see BillSearch - */ -public interface BillService extends OpenmrsService { - - /** - * Retrieves a bill by its database ID. - * - * @param id the database ID of the bill - * @return the bill with the specified ID, or null if not found - * @throws org.openmrs.api.APIAuthenticationException if the user lacks VIEW_BILLS privilege - */ - @Transactional(readOnly = true) - @Authorized(PrivilegeConstants.VIEW_BILLS) - Bill getBill(Integer id); - - /** - * Retrieves a bill by its UUID. - * - * @param uuid the UUID of the bill - * @return the bill with the specified UUID, or null if not found - * @throws org.openmrs.api.APIAuthenticationException if the user lacks VIEW_BILLS privilege - */ - @Transactional(readOnly = true) - @Authorized(PrivilegeConstants.VIEW_BILLS) - Bill getBillByUuid(String uuid); - - /** - * Retrieves a bill by its receipt number. - * - * @param receiptNumber the receipt number of the bill - * @return the bill with the specified receipt number, or null if not found - * @throws org.openmrs.api.APIAuthenticationException if the user lacks VIEW_BILLS privilege - */ - @Transactional(readOnly = true) - @Authorized(PrivilegeConstants.VIEW_BILLS) - Bill getBillByReceiptNumber(String receiptNumber); - - /** - * Retrieves all bills for a specific patient. - * - * @param patientUuid the UUID of the patient - * @param pagingInfo optional paging information (can be null for no paging) - * @return a list of bills for the patient, or an empty list if none found - * @throws org.openmrs.api.APIAuthenticationException if the user lacks VIEW_BILLS privilege - */ - @Transactional(readOnly = true) - @Authorized(PrivilegeConstants.VIEW_BILLS) - List getBillsByPatientUuid(String patientUuid, PagingInfo pagingInfo); - - /** - * Searches for bills using the specified search criteria. - *

- * By default, voided bills are excluded from search results unless explicitly included via - * {@link BillSearch#setIncludeVoided(Boolean)}. - *

- * - * @param billSearch the search criteria - * @param pagingInfo optional paging information (can be null for no paging) - * @return a list of bills matching the search criteria, or an empty list if none found - * @throws org.openmrs.api.APIAuthenticationException if the user lacks VIEW_BILLS privilege - * @see BillSearch - */ - @Transactional(readOnly = true) - @Authorized(PrivilegeConstants.VIEW_BILLS) - List getBills(BillSearch billSearch, PagingInfo pagingInfo); - - /** - * Generates and downloads a receipt for the specified bill. - * - * @param bill the bill for which to generate a receipt - * @return a byte array containing the receipt data (typically a PDF) - * @throws org.openmrs.api.APIAuthenticationException if the user lacks VIEW_BILLS privilege - */ - @Transactional(readOnly = true) - @Authorized(PrivilegeConstants.VIEW_BILLS) - byte[] downloadBillReceipt(Bill bill); - - /** - * Saves a bill to the database. - *

- * If the bill is new (no ID), it will be created. If it already exists, it will be updated. The - * bill's status will be synchronized based on its payments. - *

- * - * @param bill the bill to save - * @return the saved bill with updated metadata - * @throws org.openmrs.api.APIAuthenticationException if the user lacks MANAGE_BILLS privilege - * @throws IllegalArgumentException if the bill is null or invalid - */ - @Transactional - @Authorized(PrivilegeConstants.MANAGE_BILLS) - Bill saveBill(Bill bill); - - /** - * Permanently deletes a bill from the database. - *

- * Warning: This operation cannot be undone. Consider using - * {@link #voidBill(Bill, String)} instead for soft deletion. - *

- * - * @param bill the bill to permanently delete - * @throws org.openmrs.api.APIAuthenticationException if the user lacks PURGE_BILLS privilege - */ - @Authorized(PrivilegeConstants.PURGE_BILLS) - void purgeBill(Bill bill); - - /** - * Voids (soft deletes) a bill with a specified reason. - *

- * Voided bills are hidden from normal queries but remain in the database for audit purposes. Voided - * bills can be restored using {@link #unvoidBill(Bill)}. - *

- * - * @param bill the bill to void - * @param voidReason the reason for voiding the bill (required) - * @return the voided bill - * @throws org.openmrs.api.APIAuthenticationException if the user lacks DELETE_BILLS privilege - * @throws IllegalArgumentException if voidReason is null or empty - */ - @Authorized(PrivilegeConstants.DELETE_BILLS) - Bill voidBill(Bill bill, String voidReason); - - /** - * Restores a previously voided bill. - *

- * This operation removes the void flag and makes the bill visible in normal queries again. - *

- * - * @param bill the bill to restore - * @return the restored bill - * @throws org.openmrs.api.APIAuthenticationException if the user lacks DELETE_BILLS privilege - */ - @Authorized(PrivilegeConstants.DELETE_BILLS) - Bill unvoidBill(Bill bill); - -} diff --git a/api/src/main/java/org/openmrs/module/billing/api/IBillService.java b/api/src/main/java/org/openmrs/module/billing/api/IBillService.java new file mode 100644 index 00000000..cdd705aa --- /dev/null +++ b/api/src/main/java/org/openmrs/module/billing/api/IBillService.java @@ -0,0 +1,119 @@ +/* + * The contents of this file are subject to the OpenMRS Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://license.openmrs.org + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the + * License for the specific language governing rights and limitations + * under the License. + * + * Copyright (C) OpenMRS, LLC. All Rights Reserved. + */ + +package org.openmrs.module.billing.api; + +import java.io.File; +import java.util.List; + +import org.openmrs.Patient; +import org.openmrs.annotation.Authorized; +import org.openmrs.module.billing.api.base.PagingInfo; +import org.openmrs.module.billing.api.base.entity.IEntityDataService; +import org.openmrs.module.billing.api.model.Bill; +import org.openmrs.module.billing.api.search.BillSearch; +import org.openmrs.module.billing.api.util.PrivilegeConstants; +import org.springframework.transaction.annotation.Transactional; + +/** + * Interface that represents classes which perform data operations for {@link Bill}s. + */ +@Transactional +public interface IBillService extends IEntityDataService { + + /** + * Gets the {@link Bill} with the specified receipt number or {@code null} if not found. + * + * @param receiptNumber The receipt number to search for. + * @return The {@link Bill} with the specified receipt number or {@code null}. + * @should throw IllegalArgumentException if the receipt number is null + * @should throw IllegalArgumentException if the receipt number is empty + * @should throw IllegalArgumentException if the receipt number is longer than 255 characters + * @should return the bill with the specified reciept number + * @should return null if the receipt number is not found + */ + @Transactional(readOnly = true) + @Authorized({ PrivilegeConstants.VIEW_BILLS }) + Bill getBillByReceiptNumber(String receiptNumber); + + /** + * Returns all {@link Bill}s for the specified patient with the specified paging. + * + * @param patient The {@link Patient}. + * @param paging The paging information. + * @return All of the bills for the specified patient. + * @should throw NullPointerException if patient is null + * @should return all bills for the specified patient + * @should return an empty list if the specified patient has no bills + */ + List getBillsByPatient(Patient patient, PagingInfo paging); + + /** + * Returns all {@link Bill}s for the specified patient with the specified paging. + * + * @param patientId The patient id. + * @param paging The paging information. + * @return All of the bills for the specified patient. + * @should throw IllegalArgumentException if the patientId is less than zero + * @should throw NullPointerException if patient is null + * @should return all bills for the specified patient + * @should return an empty list if the specified patient has no bills + */ + List getBillsByPatientId(int patientId, PagingInfo paging); + + /** + * Gets all bills using the specified {@link BillSearch} settings. + * + * @param billSearch The bill search settings. + * @return The bills found or an empty list if no bills were found. + */ + @Transactional(readOnly = true) + @Authorized({ PrivilegeConstants.VIEW_BILLS }) + List getBills(BillSearch billSearch); + + /** + * Gets all bills using the specified {@link BillSearch} settings. + * + * @param billSearch The bill search settings. + * @param pagingInfo The paging information. + * @return The bills found or an empty list if no bills were found. + * @should throw NullPointerException if bill search is null + * @should throw NullPointerException if bill search template object is null + * @should return an empty list if no bills are found via the search + * @should return bills filtered by cashier + * @should return bills filtered by cash point + * @should return bills filtered by patient + * @should return bills filtered by status + * @should return all bills if paging is null + * @should return paged bills if paging is specified + * @should not return retired bills from search unless specified + */ + @Transactional(readOnly = true) + @Authorized({ PrivilegeConstants.VIEW_BILLS }) + List getBills(BillSearch billSearch, PagingInfo pagingInfo); + + @Override + @Authorized(PrivilegeConstants.VIEW_BILLS) + Bill getByUuid(String uuid); + + /** + * Gets bill receipt using the specified {@link Bill} settings. + * + * @param bill The bill search settings. + * @return The receipt containing bill items. + */ + @Transactional(readOnly = true) + @Authorized({ PrivilegeConstants.VIEW_BILLS }) + byte[] downloadBillReceipt(Bill bill); +} diff --git a/api/src/main/java/org/openmrs/module/billing/api/IBillableItemsService.java b/api/src/main/java/org/openmrs/module/billing/api/IBillableItemsService.java index d827a1d9..920b6117 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/IBillableItemsService.java +++ b/api/src/main/java/org/openmrs/module/billing/api/IBillableItemsService.java @@ -15,13 +15,13 @@ import java.util.List; -import org.openmrs.module.billing.api.base.entity.IMetadataDataService; +import org.openmrs.module.billing.api.base.entity.IEntityDataService; import org.openmrs.module.billing.api.model.BillableService; import org.openmrs.module.billing.api.search.BillableServiceSearch; import org.springframework.transaction.annotation.Transactional; @Transactional -public interface IBillableItemsService extends IMetadataDataService { +public interface IBillableItemsService extends IEntityDataService { List findServices(final BillableServiceSearch search); } diff --git a/api/src/main/java/org/openmrs/module/billing/api/ICashierItemPriceService.java b/api/src/main/java/org/openmrs/module/billing/api/ICashierItemPriceService.java index b530d6e3..385831f6 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/ICashierItemPriceService.java +++ b/api/src/main/java/org/openmrs/module/billing/api/ICashierItemPriceService.java @@ -13,9 +13,9 @@ */ package org.openmrs.module.billing.api; -import org.openmrs.module.billing.api.base.entity.IMetadataDataService; +import org.openmrs.module.billing.api.base.entity.IEntityDataService; import org.openmrs.module.billing.api.model.CashierItemPrice; import org.springframework.transaction.annotation.Transactional; @Transactional -public interface ICashierItemPriceService extends IMetadataDataService {} +public interface ICashierItemPriceService extends IEntityDataService {} diff --git a/api/src/main/java/org/openmrs/module/billing/api/ItemPriceService.java b/api/src/main/java/org/openmrs/module/billing/api/ItemPriceService.java index 76d9511c..e1208085 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/ItemPriceService.java +++ b/api/src/main/java/org/openmrs/module/billing/api/ItemPriceService.java @@ -15,16 +15,16 @@ import java.util.List; -import org.openmrs.module.billing.api.base.entity.IMetadataDataService; +import org.openmrs.module.billing.api.base.entity.IEntityDataService; import org.openmrs.module.billing.api.model.BillableService; import org.openmrs.module.billing.api.model.CashierItemPrice; import org.openmrs.module.stockmanagement.api.model.StockItem; import org.springframework.transaction.annotation.Transactional; @Transactional -public interface ItemPriceService extends IMetadataDataService { +public interface ItemPriceService extends IEntityDataService { - CashierItemPrice saveBill(CashierItemPrice price); + CashierItemPrice save(CashierItemPrice price); List getItemPrice(StockItem stockItem); diff --git a/api/src/main/java/org/openmrs/module/billing/api/base/PagingInfo.java b/api/src/main/java/org/openmrs/module/billing/api/base/PagingInfo.java index 414ceffc..5cde2fa3 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/base/PagingInfo.java +++ b/api/src/main/java/org/openmrs/module/billing/api/base/PagingInfo.java @@ -13,19 +13,10 @@ */ package org.openmrs.module.billing.api.base; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - /** * This class contains the paging information used by the entity services to paginate results. Both * page and pageSize are 1-based, defining either as 0 will cause paging to be ignored. */ -@Data -@NoArgsConstructor -@AllArgsConstructor -@Builder public class PagingInfo { private int page; @@ -36,6 +27,9 @@ public class PagingInfo { private boolean loadRecordCount; + public PagingInfo() { + } + /** * Creates a new {@link PagingInfo} instance. * @@ -45,15 +39,45 @@ public class PagingInfo { public PagingInfo(int page, int pageSize) { this.page = page; this.pageSize = pageSize; + this.loadRecordCount = true; } + public int getPage() { + return page; + } + + public void setPage(int page) { + this.page = page; + } + + public int getPageSize() { + return pageSize; + } + + public void setPageSize(int pageSize) { + this.pageSize = pageSize; + } + + public Long getTotalRecordCount() { + return totalRecordCount; + } + public void setTotalRecordCount(Long totalRecordCount) { this.totalRecordCount = totalRecordCount; + // If the total records is set to anything other than null, than don't reload the count this.loadRecordCount = totalRecordCount == null; } + public boolean shouldLoadRecordCount() { + return loadRecordCount; + } + + public void setLoadRecordCount(boolean loadRecordCount) { + this.loadRecordCount = loadRecordCount; + } + public Boolean hasMoreResults() { return ((long) page * pageSize) < totalRecordCount; } diff --git a/api/src/main/java/org/openmrs/module/billing/api/base/entity/IObjectDataService.java b/api/src/main/java/org/openmrs/module/billing/api/base/entity/IObjectDataService.java index b994f2ad..9a6e255c 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/base/entity/IObjectDataService.java +++ b/api/src/main/java/org/openmrs/module/billing/api/base/entity/IObjectDataService.java @@ -50,7 +50,7 @@ public interface IObjectDataService extends OpenmrsServ * @should update the object successfully * @should create the object successfully */ - E saveBill(E object); + E save(E object); /** * Saves an object to the database along with the specified related {@link OpenmrsObject}'s within a diff --git a/api/src/main/java/org/openmrs/module/billing/api/base/entity/impl/BaseEntityDataServiceImpl.java b/api/src/main/java/org/openmrs/module/billing/api/base/entity/impl/BaseEntityDataServiceImpl.java index d84e8fd8..4e06f094 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/base/entity/impl/BaseEntityDataServiceImpl.java +++ b/api/src/main/java/org/openmrs/module/billing/api/base/entity/impl/BaseEntityDataServiceImpl.java @@ -68,7 +68,7 @@ public void apply(OpenmrsData data) { if (!updatedObjects.isEmpty()) { return saveAll(entity, updatedObjects); } else { - return saveBill(entity); + return save(entity); } } @@ -104,7 +104,7 @@ public void apply(OpenmrsData data) { if (!updatedObjects.isEmpty()) { return saveAll(entity, updatedObjects); } else { - return saveBill(entity); + return save(entity); } } diff --git a/api/src/main/java/org/openmrs/module/billing/api/base/entity/impl/BaseMetadataDataServiceImpl.java b/api/src/main/java/org/openmrs/module/billing/api/base/entity/impl/BaseMetadataDataServiceImpl.java index f798fd64..814a93bd 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/base/entity/impl/BaseMetadataDataServiceImpl.java +++ b/api/src/main/java/org/openmrs/module/billing/api/base/entity/impl/BaseMetadataDataServiceImpl.java @@ -78,7 +78,7 @@ public void apply(OpenmrsMetadata metadata) { if (!updatedObjects.isEmpty()) { return saveAll(entity, updatedObjects); } else { - return saveBill(entity); + return save(entity); } } @@ -122,7 +122,7 @@ public void apply(OpenmrsMetadata metadata) { if (!updatedObjects.isEmpty()) { return saveAll(entity, updatedObjects); } else { - return saveBill(entity); + return save(entity); } } diff --git a/api/src/main/java/org/openmrs/module/billing/api/base/entity/impl/BaseObjectDataServiceImpl.java b/api/src/main/java/org/openmrs/module/billing/api/base/entity/impl/BaseObjectDataServiceImpl.java index e0362bf1..c0850bcf 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/base/entity/impl/BaseObjectDataServiceImpl.java +++ b/api/src/main/java/org/openmrs/module/billing/api/base/entity/impl/BaseObjectDataServiceImpl.java @@ -102,7 +102,7 @@ public void setRepository(BaseHibernateRepository repository) { @Override @Transactional - public E saveBill(E object) { + public E save(E object) { P privileges = getPrivileges(); if (privileges != null && !StringUtils.isEmpty(privileges.getSavePrivilege())) { PrivilegeUtil.requirePrivileges(Context.getAuthenticatedUser(), privileges.getSavePrivilege()); @@ -320,7 +320,7 @@ protected void loadPagingTotal(PagingInfo pagingInfo, Criteria criteria) { criteria = repository.createCriteria(getEntityClass()); } - if (pagingInfo.getLoadRecordCount()) { + if (pagingInfo.shouldLoadRecordCount()) { // Copy the current projection and transformer which requires getting access to the underlying criteria // implementation Projection projection = null; diff --git a/api/src/main/java/org/openmrs/module/billing/api/base/entity/model/BaseCustomizableData.java b/api/src/main/java/org/openmrs/module/billing/api/base/entity/model/BaseCustomizableData.java index 80cd711f..7a059225 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/base/entity/model/BaseCustomizableData.java +++ b/api/src/main/java/org/openmrs/module/billing/api/base/entity/model/BaseCustomizableData.java @@ -30,7 +30,7 @@ public abstract class BaseCustomizableData> // @formatter:on public static final long serialVersionUID = 0L; - private Set attributes = new HashSet<>(); + private Set attributes; protected void onAddAttribute(TAttribute attribute) { // Just here to allow subclass to add custom logic diff --git a/api/src/main/java/org/openmrs/module/billing/api/db/BillDAO.java b/api/src/main/java/org/openmrs/module/billing/api/db/BillDAO.java deleted file mode 100644 index 8bca90ff..00000000 --- a/api/src/main/java/org/openmrs/module/billing/api/db/BillDAO.java +++ /dev/null @@ -1,110 +0,0 @@ -package org.openmrs.module.billing.api.db; - -import org.openmrs.module.billing.api.base.PagingInfo; -import org.openmrs.module.billing.api.model.Bill; -import org.openmrs.module.billing.api.search.BillSearch; -import org.springframework.transaction.annotation.Transactional; - -import javax.annotation.Nonnull; -import java.util.List; - -/** - * Data Access Object (DAO) interface for {@link Bill} persistence operations. - * - * @see Bill - * @see BillSearch - */ -public interface BillDAO { - - /** - * Retrieves a bill by its database ID. - * - * @param id the database ID of the bill (must not be null) - * @return the bill with the specified ID, or null if not found - */ - @Transactional(readOnly = true) - Bill getBill(@Nonnull Integer id); - - /** - * Retrieves a bill by its UUID. - *

- * Note: This method may return voided bills. Consider filtering voided records at the service layer - * if needed. - *

- * - * @param uuid the UUID of the bill (must not be null) - * @return the bill with the specified UUID, or null if not found - */ - @Transactional(readOnly = true) - Bill getBillByUuid(@Nonnull String uuid); - - /** - * Persists a bill to the database. - *

- * If the bill has no ID, it will be created as a new record. If it has an ID, the existing record - * will be updated. - *

- * - * @param bill the bill to save (must not be null) - * @return the saved bill with updated metadata (timestamps, IDs, etc.) - */ - @Transactional - Bill saveBill(@Nonnull Bill bill); - - /** - * Retrieves a bill by its receipt number. - *

- * Note: This method may return voided bills. Consider filtering voided records at the service layer - * if needed. - *

- * - * @param receiptNumber the receipt number of the bill (must not be null) - * @return the bill with the specified receipt number, or null if not found - */ - @Transactional(readOnly = true) - Bill getBillByReceiptNumber(@Nonnull String receiptNumber); - - /** - * Retrieves all bills for a specific patient. - *

- * Note: This method may return voided bills. Consider filtering voided records at the service layer - * if needed. - *

- * - * @param patientUuid the UUID of the patient (must not be null) - * @param pagingInfo optional paging information (can be null for no paging) - * @return a list of bills for the patient, or an empty list if none found - */ - @Transactional(readOnly = true) - List getBillsByPatientUuid(@Nonnull String patientUuid, PagingInfo pagingInfo); - - /** - * Searches for bills using the specified search criteria. - *

- * By default, voided bills are excluded from results unless - * {@link BillSearch#setIncludeVoided(Boolean)} is set to true. The search criteria support - * filtering by patient, cashier, cash point, and status. - *

- * - * @param billSearch the search criteria (must not be null) - * @param pagingInfo optional paging information (can be null for no paging). When provided with - * {@code loadRecordCount=true}, the total count will be populated in the pagingInfo - * @return a list of bills matching the search criteria, or an empty list if none found - * @see BillSearch - */ - @Transactional(readOnly = true) - List getBills(@Nonnull BillSearch billSearch, PagingInfo pagingInfo); - - /** - * Permanently deletes a bill from the database. - *

- * Warning: This operation cannot be undone. All associated data (line items, - * payments, etc.) will also be removed due to cascade delete rules. - *

- * - * @param bill the bill to permanently delete (must not be null) - */ - @Transactional - void purgeBill(@Nonnull Bill bill); - -} diff --git a/api/src/main/java/org/openmrs/module/billing/api/db/BillExemptionDAO.java b/api/src/main/java/org/openmrs/module/billing/api/db/BillExemptionDAO.java deleted file mode 100644 index c9581215..00000000 --- a/api/src/main/java/org/openmrs/module/billing/api/db/BillExemptionDAO.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.openmrs.module.billing.api.db; - -import org.openmrs.Concept; -import org.openmrs.module.billing.api.model.BillExemption; -import org.openmrs.module.billing.api.model.ExemptionType; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; - -public interface BillExemptionDAO { - - BillExemption save(BillExemption billExemption); - - BillExemption getBillingExemptionById(Integer id); - - @Transactional(readOnly = true) - BillExemption getBillingExemptionByUuid(String uuid); - - @Transactional(readOnly = true) - List getExemptionsByConcept(Concept concept, ExemptionType itemType, boolean includeRetired); - - @Transactional(readOnly = true) - List getExemptionsByItemType(ExemptionType itemType, boolean includeRetired); -} diff --git a/api/src/main/java/org/openmrs/module/billing/api/db/hibernate/BillExemptionDAOImpl.java b/api/src/main/java/org/openmrs/module/billing/api/db/hibernate/BillExemptionDAOImpl.java deleted file mode 100644 index 15e98d0f..00000000 --- a/api/src/main/java/org/openmrs/module/billing/api/db/hibernate/BillExemptionDAOImpl.java +++ /dev/null @@ -1,94 +0,0 @@ -package org.openmrs.module.billing.api.db.hibernate; - -import org.hibernate.Session; -import org.hibernate.SessionFactory; -import org.openmrs.Concept; -import org.openmrs.module.billing.api.db.BillExemptionDAO; -import org.openmrs.module.billing.api.model.BillExemption; -import org.openmrs.module.billing.api.model.ExemptionType; - -import javax.persistence.criteria.CriteriaBuilder; -import javax.persistence.criteria.CriteriaQuery; -import javax.persistence.criteria.Predicate; -import javax.persistence.criteria.Root; -import java.util.ArrayList; -import java.util.List; - -public class BillExemptionDAOImpl implements BillExemptionDAO { - - private final SessionFactory sessionFactory; - - public BillExemptionDAOImpl(SessionFactory sessionFactory) { - this.sessionFactory = sessionFactory; - } - - @Override - public BillExemption save(BillExemption billExemption) { - sessionFactory.getCurrentSession().saveOrUpdate(billExemption); - return billExemption; - } - - @Override - public BillExemption getBillingExemptionById(Integer id) { - return sessionFactory.getCurrentSession().get(BillExemption.class, id); - } - - @Override - public BillExemption getBillingExemptionByUuid(String uuid) { - Session session = sessionFactory.getCurrentSession(); - CriteriaBuilder cb = session.getCriteriaBuilder(); - CriteriaQuery query = cb.createQuery(BillExemption.class); - Root root = query.from(BillExemption.class); - - query.select(root).where(cb.equal(root.get("uuid"), uuid)); - return session.createQuery(query).getSingleResult(); - } - - @Override - public List getExemptionsByConcept(Concept concept, ExemptionType itemType, boolean includeRetired) { - Session session = sessionFactory.getCurrentSession(); - CriteriaBuilder cb = session.getCriteriaBuilder(); - CriteriaQuery query = cb.createQuery(BillExemption.class); - Root root = query.from(BillExemption.class); - - List predicates = new ArrayList<>(); - - if (concept != null) { - predicates.add(cb.equal(root.get("concept"), concept)); - } - - if (itemType != null) { - predicates.add(cb.equal(root.get("exemptionType"), itemType)); - } - - if (!includeRetired) { - predicates.add(cb.isFalse(root.get("retired"))); - } - - query.where(predicates.toArray(new Predicate[0])); - - return session.createQuery(query).getResultList(); - } - - @Override - public List getExemptionsByItemType(ExemptionType itemType, boolean includeRetired) { - Session session = sessionFactory.getCurrentSession(); - CriteriaBuilder cb = session.getCriteriaBuilder(); - CriteriaQuery query = cb.createQuery(BillExemption.class); - Root root = query.from(BillExemption.class); - - List predicates = new ArrayList<>(); - if (itemType != null) { - predicates.add(cb.equal(root.get("exemptionType"), itemType)); - } - - if (!includeRetired) { - predicates.add(cb.isFalse(root.get("retired"))); - } - - query.where(predicates.toArray(new Predicate[0])); - - return session.createQuery(query).getResultList(); - } - -} diff --git a/api/src/main/java/org/openmrs/module/billing/api/db/hibernate/HibernateBillDAOImpl.java b/api/src/main/java/org/openmrs/module/billing/api/db/hibernate/HibernateBillDAOImpl.java deleted file mode 100644 index 303e6be7..00000000 --- a/api/src/main/java/org/openmrs/module/billing/api/db/hibernate/HibernateBillDAOImpl.java +++ /dev/null @@ -1,191 +0,0 @@ -package org.openmrs.module.billing.api.db.hibernate; - -import org.apache.commons.lang3.StringUtils; -import org.openmrs.Patient; -import org.openmrs.api.context.Context; -import org.openmrs.api.db.hibernate.HibernatePatientDAO; -import org.openmrs.module.billing.api.base.PagingInfo; -import org.openmrs.module.billing.api.db.BillDAO; -import org.openmrs.module.billing.api.model.Bill; -import org.openmrs.module.billing.api.search.BillSearch; - -import javax.annotation.Nonnull; -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; -import javax.persistence.TypedQuery; -import javax.persistence.criteria.CriteriaBuilder; -import javax.persistence.criteria.CriteriaQuery; -import javax.persistence.criteria.Predicate; -import javax.persistence.criteria.Root; -import java.util.ArrayList; -import java.util.List; - -/** - * Hibernate implementation of {@link BillDAO}. - * - * @see BillDAO - * @see Bill - */ -public class HibernateBillDAOImpl implements BillDAO { - - @PersistenceContext - private EntityManager entityManager; - - /** - * {@inheritDoc} - */ - @Override - public Bill getBill(@Nonnull Integer id) { - return entityManager.find(Bill.class, id); - } - - /** - * {@inheritDoc} - */ - @Override - public Bill getBillByUuid(@Nonnull String uuid) { - TypedQuery query = entityManager.createQuery("select b from Bill b where b.uuid = :uuid", Bill.class); - query.setParameter("uuid", uuid); - return query.getResultStream().findFirst().orElse(null); - } - - /** - * {@inheritDoc} - */ - @Override - public Bill saveBill(@Nonnull Bill bill) { - if (bill.getId() == null) { - entityManager.persist(bill); - return bill; - } - return entityManager.merge(bill); - } - - /** - * {@inheritDoc} - */ - @Override - public Bill getBillByReceiptNumber(@Nonnull String receiptNumber) { - TypedQuery query = entityManager.createQuery("select b from Bill b where b.receiptNumber = :receiptNumber", - Bill.class); - query.setParameter("receiptNumber", receiptNumber); - return query.getResultStream().findFirst().orElse(null); - } - - /** - * {@inheritDoc} - */ - @Override - public List getBillsByPatientUuid(@Nonnull String patientUuid, PagingInfo pagingInfo) { - CriteriaBuilder cb = entityManager.getCriteriaBuilder(); - CriteriaQuery cq = cb.createQuery(Bill.class); - Root root = cq.from(Bill.class); - - Predicate predicate = cb.equal(root.get("patient").get("uuid"), patientUuid); - cq.where(predicate); - - TypedQuery query = entityManager.createQuery(cq); - - List predicates = new ArrayList<>(); - predicates.add(predicate); - applyPaging(query, pagingInfo, predicates); - - return query.getResultList(); - } - - /** - * {@inheritDoc} - */ - @Override - public List getBills(@Nonnull BillSearch billSearch, PagingInfo pagingInfo) { - CriteriaBuilder cb = entityManager.getCriteriaBuilder(); - CriteriaQuery cq = cb.createQuery(Bill.class); - Root root = cq.from(Bill.class); - - List predicates = buildBillSearchPredicate(cb, root, billSearch); - - if (!predicates.isEmpty()) { - cq.where(predicates.toArray(new Predicate[0])); - } - - TypedQuery query = entityManager.createQuery(cq); - - applyPaging(query, pagingInfo, predicates); - - return query.getResultList(); - } - - /** - * {@inheritDoc} - */ - @Override - public void purgeBill(@Nonnull Bill bill) { - entityManager.remove(bill); - } - - private List buildBillSearchPredicate(CriteriaBuilder cb, Root root, BillSearch billSearch) { - List predicates = new ArrayList<>(); - - if (billSearch.getPatientUuid() != null) { - predicates.add(cb.equal(root.get("patient").get("uuid"), billSearch.getPatientUuid())); - } - - if (billSearch.getPatientName() != null && !billSearch.getPatientName().trim().isEmpty()) { - List matchingPatients = Context.getRegisteredComponent("patientDAO", HibernatePatientDAO.class) - .getPatients(billSearch.getPatientName(), 0, null); - if (matchingPatients != null && !matchingPatients.isEmpty()) { - predicates.add(root.get("patient").in(matchingPatients)); - } else { - predicates.add(cb.disjunction()); - } - } - - if (StringUtils.isNotEmpty(billSearch.getCashierUuid())) { - predicates.add(cb.equal(root.get("cashier").get("uuid"), billSearch.getCashierUuid())); - } - - if (billSearch.getCashPointUuid() != null) { - predicates.add(cb.equal(root.get("cashPoint").get("uuid"), billSearch.getCashPointUuid())); - } - - if (billSearch.getStatuses() != null && !billSearch.getStatuses().isEmpty()) { - predicates.add(root.get("status").in(billSearch.getStatuses())); - } - - if (!Boolean.TRUE.equals(billSearch.getIncludeVoided())) { - predicates.add(cb.equal(root.get("voided"), false)); - } - - return predicates; - } - - /** - * Applies paging to a query and optionally loads total record count. - * - * @param query The typed query to apply paging to - * @param pagingInfo The paging information (null to skip paging) - * @param predicates The predicates used for filtering (needed for count query) - */ - private void applyPaging(TypedQuery query, PagingInfo pagingInfo, List predicates) { - if (pagingInfo != null && pagingInfo.getPage() > 0 && pagingInfo.getPageSize() > 0) { - int offset = (pagingInfo.getPage() - 1) * pagingInfo.getPageSize(); - query.setFirstResult(offset); - query.setMaxResults(pagingInfo.getPageSize()); - - if (pagingInfo.getLoadRecordCount()) { - CriteriaBuilder cb = entityManager.getCriteriaBuilder(); - CriteriaQuery countQuery = cb.createQuery(Long.class); - Root countRoot = countQuery.from(Bill.class); - countQuery.select(cb.count(countRoot)); - - if (predicates != null && !predicates.isEmpty()) { - countQuery.where(predicates.toArray(new Predicate[0])); - } - - Long totalCount = entityManager.createQuery(countQuery).getSingleResult(); - pagingInfo.setTotalRecordCount(totalCount); - } - } - } - -} diff --git a/api/src/main/java/org/openmrs/module/billing/api/evaluator/ExemptionEvaluator.java b/api/src/main/java/org/openmrs/module/billing/api/evaluator/ExemptionEvaluator.java deleted file mode 100644 index 96ff36fe..00000000 --- a/api/src/main/java/org/openmrs/module/billing/api/evaluator/ExemptionEvaluator.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.openmrs.module.billing.api.evaluator; - -import java.util.Map; - -public interface ExemptionEvaluator { - - ScriptType getSupportedType(); - - boolean evaluate(String script, Map variables); - -} diff --git a/api/src/main/java/org/openmrs/module/billing/api/evaluator/ExemptionRuleEngine.java b/api/src/main/java/org/openmrs/module/billing/api/evaluator/ExemptionRuleEngine.java deleted file mode 100644 index cd900c8d..00000000 --- a/api/src/main/java/org/openmrs/module/billing/api/evaluator/ExemptionRuleEngine.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.openmrs.module.billing.api.evaluator; - -import org.openmrs.module.billing.api.model.BillExemption; -import org.openmrs.module.billing.api.model.BillExemptionRule; - -import java.util.EnumMap; -import java.util.List; -import java.util.Map; - -public class ExemptionRuleEngine { - - private final Map evaluatorsByType = new EnumMap<>(ScriptType.class); - - public ExemptionRuleEngine(List evaluators) { - for (ExemptionEvaluator evaluator : evaluators) { - evaluatorsByType.put(evaluator.getSupportedType(), evaluator); - } - } - - public boolean evaluateRule(BillExemptionRule rule, Map variables) { - ExemptionEvaluator evaluator = evaluatorsByType.get(rule.getScriptType()); - if (evaluator == null) { - throw new IllegalArgumentException("Unsupported script type: " + rule.getScriptType()); - } - return evaluator.evaluate(rule.getScript(), variables); - } - - public boolean isExemptionApplicable(BillExemption exemption, Map variables) { - if (exemption.getRules() == null || exemption.getRules().isEmpty()) { - return false; - } - - return exemption.getRules().stream().filter(r -> !r.getVoided()).anyMatch(r -> evaluateRule(r, variables)); - } -} diff --git a/api/src/main/java/org/openmrs/module/billing/api/evaluator/ScriptType.java b/api/src/main/java/org/openmrs/module/billing/api/evaluator/ScriptType.java deleted file mode 100644 index df2bea92..00000000 --- a/api/src/main/java/org/openmrs/module/billing/api/evaluator/ScriptType.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.openmrs.module.billing.api.evaluator; - -public enum ScriptType { - - JAVASCRIPT("js"); - - private final String engineName; - - ScriptType(String engineName) { - this.engineName = engineName; - } - - public String getEngineName() { - return engineName; - } -} diff --git a/api/src/main/java/org/openmrs/module/billing/api/evaluator/impl/JSExemptionEvaluator.java b/api/src/main/java/org/openmrs/module/billing/api/evaluator/impl/JSExemptionEvaluator.java deleted file mode 100644 index 5082211b..00000000 --- a/api/src/main/java/org/openmrs/module/billing/api/evaluator/impl/JSExemptionEvaluator.java +++ /dev/null @@ -1,67 +0,0 @@ -package org.openmrs.module.billing.api.evaluator.impl; - -import org.apache.commons.lang.StringEscapeUtils; -import org.graalvm.polyglot.Context; -import org.graalvm.polyglot.Value; -import org.openmrs.module.billing.api.evaluator.ExemptionEvaluator; -import org.openmrs.module.billing.api.evaluator.ScriptType; - -import java.util.Collections; -import java.util.Map; - -public class JSExemptionEvaluator implements ExemptionEvaluator { - - @Override - public ScriptType getSupportedType() { - return ScriptType.JAVASCRIPT; - } - - @Override - public boolean evaluate(String script, Map variables) { - try (Context context = Context.newBuilder("js").allowAllAccess(false).allowHostClassLookup(className -> false) - .build()) { - Value bindings = context.getBindings("js"); - - Map safeVars = (variables != null ? variables : Collections.emptyMap()); - - Value varsObject = convertMapToJSObject(context, safeVars); - bindings.putMember("vars", varsObject); - - for (Map.Entry entry : safeVars.entrySet()) { - Object value = entry.getValue(); - if (value instanceof Map) { - value = convertMapToJSObject(context, (Map) value); - } - bindings.putMember(entry.getKey(), value); - } - - Value result = context.eval("js", script); - - if (result.isBoolean()) { - return result.asBoolean(); - } - if (result.isNull()) { - return false; - } - return Boolean.parseBoolean(result.toString()); - } - catch (Exception e) { - throw new RuntimeException("Error evaluating JS exemption script: " + script, e); - } - } - - private Value convertMapToJSObject(Context context, Map map) { - Value jsObject = context.eval("js", "({})"); - for (Map.Entry entry : map.entrySet()) { - String key = entry.getKey().toString(); - Object value = entry.getValue(); - - if (value instanceof Map) { - value = convertMapToJSObject(context, (Map) value); - } - - jsObject.putMember(key, value); - } - return jsObject; - } -} diff --git a/api/src/main/java/org/openmrs/module/billing/api/impl/BillExemptionServiceImpl.java b/api/src/main/java/org/openmrs/module/billing/api/impl/BillExemptionServiceImpl.java deleted file mode 100644 index 076f35b0..00000000 --- a/api/src/main/java/org/openmrs/module/billing/api/impl/BillExemptionServiceImpl.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.openmrs.module.billing.api.impl; - -import org.openmrs.Concept; -import org.openmrs.module.billing.api.BillExemptionService; -import org.openmrs.module.billing.api.db.BillExemptionDAO; -import org.openmrs.module.billing.api.model.BillExemption; -import org.openmrs.module.billing.api.model.ExemptionType; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; - -@Service("billing.billingExemptionService") -@Transactional -public class BillExemptionServiceImpl implements BillExemptionService { - - private final BillExemptionDAO billExemptionDAO; - - public BillExemptionServiceImpl(BillExemptionDAO billExemptionDAO) { - this.billExemptionDAO = billExemptionDAO; - } - - @Override - public BillExemption save(BillExemption billExemption) { - return billExemptionDAO.save(billExemption); - } - - @Override - public BillExemption getBillingExemptionById(Integer id) { - return billExemptionDAO.getBillingExemptionById(id); - } - - @Override - public BillExemption getBillingExemptionByUuid(String uuid) { - return billExemptionDAO.getBillingExemptionByUuid(uuid); - } - - @Override - public List getExemptionsByConcept(Concept concept, ExemptionType itemType, boolean includeRetired) { - return billExemptionDAO.getExemptionsByConcept(concept, itemType, includeRetired); - } - - @Override - public List getExemptionsByItemType(ExemptionType itemType, boolean includeRetired) { - return billExemptionDAO.getExemptionsByItemType(itemType, includeRetired); - } - -} diff --git a/api/src/main/java/org/openmrs/module/billing/api/impl/BillLineItemServiceImpl.java b/api/src/main/java/org/openmrs/module/billing/api/impl/BillLineItemServiceImpl.java index deb97f2c..75473c35 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/impl/BillLineItemServiceImpl.java +++ b/api/src/main/java/org/openmrs/module/billing/api/impl/BillLineItemServiceImpl.java @@ -13,12 +13,9 @@ */ package org.openmrs.module.billing.api.impl; -import org.openmrs.api.context.Context; import org.openmrs.module.billing.api.BillLineItemService; -import org.openmrs.module.billing.api.BillService; import org.openmrs.module.billing.api.base.entity.impl.BaseEntityDataServiceImpl; import org.openmrs.module.billing.api.base.entity.security.IEntityAuthorizationPrivileges; -import org.openmrs.module.billing.api.model.Bill; import org.openmrs.module.billing.api.model.BillLineItem; import org.springframework.transaction.annotation.Transactional; @@ -32,6 +29,7 @@ protected IEntityAuthorizationPrivileges getPrivileges() { @Override protected void validate(BillLineItem object) { + } @Override @@ -53,48 +51,4 @@ public String getPurgePrivilege() { public String getGetPrivilege() { return null; } - - @Override - public BillLineItem voidEntity(BillLineItem entity, String reason) { - BillLineItem voidedLineItem = super.voidEntity(entity, reason); - - if (voidedLineItem != null && voidedLineItem.getBill() != null) { - Bill bill = voidedLineItem.getBill(); - bill.synchronizeBillStatus(); - } - - return voidedLineItem; - } - - @Override - public BillLineItem unvoidEntity(BillLineItem entity) { - BillLineItem unvoidedLineItem = super.unvoidEntity(entity); - - if (unvoidedLineItem != null && unvoidedLineItem.getBill() != null) { - Bill bill = unvoidedLineItem.getBill(); - bill.synchronizeBillStatus(); - } - - return unvoidedLineItem; - } - - @Override - public void purge(BillLineItem entity) { - Bill bill = null; - if (entity != null && entity.getBill() != null) { - bill = entity.getBill(); - // Validate before purging (purge doesn't call validate()) - } - - super.purge(entity); - - if (bill != null) { - // Remove the line item from the bill's collection - bill.removeLineItem(entity); - bill.synchronizeBillStatus(); - // Save the bill to persist the collection change - BillService billService = Context.getService(BillService.class); - billService.saveBill(bill); - } - } } diff --git a/api/src/main/java/org/openmrs/module/billing/api/impl/BillServiceImpl.java b/api/src/main/java/org/openmrs/module/billing/api/impl/BillServiceImpl.java index 4882aa04..a6f2c64d 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/impl/BillServiceImpl.java +++ b/api/src/main/java/org/openmrs/module/billing/api/impl/BillServiceImpl.java @@ -13,142 +13,563 @@ */ package org.openmrs.module.billing.api.impl; -import lombok.Setter; -import org.apache.commons.lang3.StringUtils; -import org.openmrs.api.impl.BaseOpenmrsService; -import org.openmrs.module.billing.api.BillService; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.math.BigDecimal; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.AccessControlException; +import java.text.DecimalFormat; +import java.util.Date; +import java.util.List; + +import com.itextpdf.io.font.constants.StandardFonts; +import com.itextpdf.io.image.ImageDataFactory; +import com.itextpdf.kernel.font.PdfFont; +import com.itextpdf.kernel.font.PdfFontFactory; +import com.itextpdf.kernel.geom.PageSize; +import com.itextpdf.kernel.geom.Rectangle; +import com.itextpdf.kernel.pdf.PdfDocument; +import com.itextpdf.kernel.pdf.PdfWriter; +import com.itextpdf.layout.Document; +import com.itextpdf.layout.borders.Border; +import com.itextpdf.layout.element.Cell; +import com.itextpdf.layout.element.IElement; +import com.itextpdf.layout.element.Image; +import com.itextpdf.layout.element.Paragraph; +import com.itextpdf.layout.element.Table; +import com.itextpdf.layout.element.Text; +import com.itextpdf.layout.properties.TextAlignment; +import com.itextpdf.layout.properties.UnitValue; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.WordUtils; +import org.hibernate.Criteria; +import org.hibernate.criterion.Order; +import org.hibernate.criterion.Restrictions; +import org.joda.time.DateTime; +import org.openmrs.GlobalProperty; +import org.openmrs.Patient; +import org.openmrs.annotation.Authorized; +import org.openmrs.api.AdministrationService; +import org.openmrs.api.context.Context; +import org.openmrs.module.billing.api.IBillService; +import org.openmrs.module.billing.api.IReceiptNumberGenerator; +import org.openmrs.module.billing.api.ReceiptNumberGeneratorFactory; import org.openmrs.module.billing.api.base.PagingInfo; -import org.openmrs.module.billing.api.db.BillDAO; +import org.openmrs.module.billing.api.base.entity.impl.BaseEntityDataServiceImpl; +import org.openmrs.module.billing.api.base.entity.security.IEntityAuthorizationPrivileges; +import org.openmrs.module.billing.api.base.f.Action1; import org.openmrs.module.billing.api.model.Bill; +import org.openmrs.module.billing.api.model.BillLineItem; +import org.openmrs.module.billing.api.model.BillStatus; +import org.openmrs.module.billing.api.model.Payment; import org.openmrs.module.billing.api.search.BillSearch; -import org.openmrs.module.billing.util.ReceiptGenerator; -import org.springframework.beans.factory.annotation.Autowired; +import org.openmrs.module.billing.api.util.PrivilegeConstants; +import org.openmrs.module.billing.util.Utils; +import org.openmrs.util.OpenmrsUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.transaction.annotation.Transactional; -import java.util.Collections; -import java.util.List; - /** - * Default implementation of {@link BillService}. - *

- * This class delegates to {@link BillDAO} for persistence operations. For detailed documentation of - * each method, see the interface {@link BillService}. - *

- * - * @see BillService - * @see BillDAO + * Data service implementation class for {@link Bill}s. */ @Transactional -public class BillServiceImpl extends BaseOpenmrsService implements BillService { +public class BillServiceImpl extends BaseEntityDataServiceImpl implements IEntityAuthorizationPrivileges, IBillService { - @Setter(onMethod_ = { @Autowired }) - private BillDAO billDAO; + private static final int MAX_LENGTH_RECEIPT_NUMBER = 255; + + private static final Logger LOG = LoggerFactory.getLogger(BillServiceImpl.class); + + private static final String GP_DEFAULT_LOCATION = "defaultLocation"; + + private static final String GP_FACILITY_ADDRESS_DETAILS = "billing.receipt.facilityAddress"; + + private static final String GP_BILL_LOGO_PATH = "billing.receipt.logoPath"; - /** - * {@inheritDoc} - */ @Override - public Bill getBill(Integer id) { - if (id == null) { - return null; - } - return billDAO.getBill(id); + protected IEntityAuthorizationPrivileges getPrivileges() { + return this; } - /** - * {@inheritDoc} - */ + DecimalFormat df = new DecimalFormat("0.00"); + @Override - public Bill getBillByUuid(String uuid) { - if (uuid == null) { - return null; - } - return billDAO.getBillByUuid(uuid); + protected void validate(Bill bill) { } /** - * {@inheritDoc} + * Saves the bill to the database, creating a new bill or updating an existing one. + * + * @param bill The bill to be saved. + * @return The saved bill. + * @should Generate a new receipt number if one has not been defined. + * @should Not generate a receipt number if one has already been defined. + * @should Throw APIException if receipt number cannot be generated. */ @Override - public Bill saveBill(Bill bill) { + @Authorized({ PrivilegeConstants.MANAGE_BILLS }) + @Transactional + public Bill save(Bill bill) { if (bill == null) { throw new NullPointerException("The bill must be defined."); } - return billDAO.saveBill(bill); + + // Check for refund. + // A refund is given when the total of the bill's line items is negative. + if (bill.getTotal().compareTo(BigDecimal.ZERO) < 0 && !Context.hasPrivilege(PrivilegeConstants.REFUND_MONEY)) { + throw new AccessControlException("Access denied to give a refund."); + } + + // Generate a receipt number if it hasn't been defined + IReceiptNumberGenerator generator = ReceiptNumberGeneratorFactory.getGenerator(); + if (generator == null) { + LOG.warn( + "No receipt number generator has been defined. Bills will not be given a receipt number until one is defined."); + } else { + if (StringUtils.isEmpty(bill.getReceiptNumber())) { + bill.setReceiptNumber(generator.generateNumber(bill)); + } + } + // Check if there is an existing pending bill for the patient + List bills = searchBill(bill.getPatient()); + if (!bills.isEmpty()) { + Bill billToUpdate = bills.get(0); + billToUpdate.setStatus(BillStatus.PENDING); + for (BillLineItem item : bill.getLineItems()) { + item.setBill(billToUpdate); + billToUpdate.getLineItems().add(item); + } + + // Calculate the total payments made on the bill + BigDecimal totalPaid = billToUpdate.getPayments().stream().map(Payment::getAmountTendered) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + // Check if the bill is fully paid + if (totalPaid.compareTo(billToUpdate.getTotal()) >= 0) { + billToUpdate.setStatus(BillStatus.PAID); + } else { + billToUpdate.setStatus(BillStatus.PENDING); + } + + // Save the updated bill + return super.save(billToUpdate); + } + + // If no pending bill exists, just save the new bill as it is + return super.save(bill); } - /** - * {@inheritDoc} - */ @Override + @Authorized({ PrivilegeConstants.VIEW_BILLS }) + @Transactional(readOnly = true) public Bill getBillByReceiptNumber(String receiptNumber) { - if (receiptNumber == null) { - return null; + if (StringUtils.isEmpty(receiptNumber)) { + throw new IllegalArgumentException("The receipt number must be defined."); + } + if (receiptNumber.length() > MAX_LENGTH_RECEIPT_NUMBER) { + throw new IllegalArgumentException("The receipt number must be less than 256 characters."); } - return billDAO.getBillByReceiptNumber(receiptNumber); + + Criteria criteria = getRepository().createCriteria(getEntityClass()); + criteria.add(Restrictions.eq("receiptNumber", receiptNumber)); + + Bill bill = getRepository().selectSingle(getEntityClass(), criteria); + removeNullLineItems(bill); + return bill; } - /** - * {@inheritDoc} - */ @Override - public List getBillsByPatientUuid(String patientUuid, PagingInfo pagingInfo) { - if (StringUtils.isEmpty(patientUuid)) { - return Collections.emptyList(); + public List getBillsByPatient(Patient patient, PagingInfo paging) { + if (patient == null) { + throw new NullPointerException("The patient must be defined."); } - return billDAO.getBillsByPatientUuid(patientUuid, pagingInfo); + + return getBillsByPatientId(patient.getId(), paging); + } + + @Override + public List getBillsByPatientId(int patientId, PagingInfo paging) { + if (patientId < 0) { + throw new IllegalArgumentException("The patient id must be a valid identifier."); + } + + Criteria criteria = getRepository().createCriteria(getEntityClass()); + criteria.add(Restrictions.eq("patient.id", patientId)); + criteria.addOrder(Order.desc("id")); + + List results = getRepository().select(getEntityClass(), createPagingCriteria(paging, criteria)); + removeNullLineItems(results); + + return results; + } + + @Override + public List getBills(final BillSearch billSearch) { + return getBills(billSearch, null); } - /** - * {@inheritDoc} - */ @Override - public List getBills(BillSearch billSearch, PagingInfo pagingInfo) { + public List getBills(final BillSearch billSearch, PagingInfo pagingInfo) { if (billSearch == null) { - return Collections.emptyList(); + throw new NullPointerException("The bill search must be defined."); + } else if (billSearch.getTemplate() == null) { + throw new NullPointerException("The bill search template must be defined."); } - return billDAO.getBills(billSearch, pagingInfo); + + return executeCriteria(Bill.class, pagingInfo, new Action1() { + + @Override + public void apply(Criteria criteria) { + billSearch.updateCriteria(criteria); + } + }); + } + + /* + These methods are overridden to ensure that any null line items (created as part of a bug in 1.7.0) are removed + from the results before being returned to the caller. + */ + @Override + public List getAll(boolean includeVoided, PagingInfo pagingInfo) { + List results = super.getAll(includeVoided, pagingInfo); + removeNullLineItems(results); + return results; + } + + @Override + public Bill getById(int entityId) { + Bill bill = super.getById(entityId); + removeNullLineItems(bill); + return bill; + } + + @Override + public Bill getByUuid(String uuid) { + Bill bill = super.getByUuid(uuid); + removeNullLineItems(bill); + return bill; } /** - * {@inheritDoc} + * Generate a pdf receipt + * + * @param bill The bill search settings. + * @return */ @Override public byte[] downloadBillReceipt(Bill bill) { - if (bill == null) { - throw new NullPointerException("The bill must be defined."); + AdministrationService administrationService = Context.getAdministrationService(); + Patient patient = bill.getPatient(); + String fullName = patient.getPersonName().getFullName(); + String gender = patient.getGender() != null ? patient.getGender() : ""; + String dob = patient.getBirthdate() != null ? Utils.getSimpleDateFormat("dd-MMM-yyyy").format(patient.getBirthdate()) + : ""; + + /** + * https://kb.itextpdf.com/home/it7kb/faq/how-to-set-the-page-size-to-envelope-size-with-landscape-orientation + * page size: 3.5inch length, 1.1 inch height 1mm = 0.0394 inch length = 450mm = 17.7165 inch = + * 127.5588 points height = 300mm = 11.811 inch = 85.0392 points The measurement system in PDF + * doesn't use inches, but user units. By default, 1 user unit = 1 point, and 1 inch = 72 points. + * Thermal printer: 4 x 10 inches paper 4 inches = 4 x 72 = 288 5 inches = 10 x 72 = 720 + */ + int FONT_SIZE_12 = 12; + Rectangle thermalPrinterPageSize = new Rectangle(288, 14400); + + PdfFont timesRoman; + PdfFont courierBold; + PdfFont helvetica; + PdfFont helveticaBold; + try { + timesRoman = PdfFontFactory.createFont(StandardFonts.TIMES_ROMAN); + courierBold = PdfFontFactory.createFont(StandardFonts.COURIER_BOLD); + helvetica = PdfFontFactory.createFont(StandardFonts.HELVETICA); + helveticaBold = PdfFontFactory.createFont(StandardFonts.HELVETICA_BOLD); + + } + catch (IOException e) { + throw new RuntimeException(e); + } + + PdfFont headerSectionFont = helveticaBold; + PdfFont billItemSectionFont = helvetica; + PdfFont footerSectionFont = courierBold; + URL logoUrl = null; + + String logoPath = administrationService.getGlobalProperty(GP_BILL_LOGO_PATH, ""); + if (StringUtils.isNotBlank(logoPath)) { + File file = new File(logoPath.trim()); + if (!file.isAbsolute()) { + file = new File(OpenmrsUtil.getApplicationDataDirectory(), logoPath.trim()); + } + + if (file.exists()) { + try { + logoUrl = file.getAbsoluteFile().toURI().toURL(); + } + catch (MalformedURLException e) { + LOG.error("Error Loading file: {}", file.getAbsoluteFile(), e); + } + } + } + + if (logoUrl == null) { + logoUrl = BillServiceImpl.class.getClassLoader().getResource("img/openmrs-logo.png"); + } + + Image logoImage = null; + if (logoUrl != null) { + logoImage = new Image(ImageDataFactory.create(logoUrl)); + logoImage.scaleToFit(80, 80); + } + Paragraph divider = new Paragraph("------------------------------------------------------------------"); + Text billDateLabel = new Text(Utils.getSimpleDateFormat("dd-MMM-yyyy HH:mm:ss").format(bill.getDateCreated())); + + GlobalProperty gp = administrationService.getGlobalPropertyObject(GP_DEFAULT_LOCATION); + //GlobalProperty gpFacilityAddress = Context.getAdministrationService().getGlobalPropertyObject(GP_FACILITY_ADDRESS_DETAILS); + //Text facilityName = new Text(gp != null && gp.getValue() != null ? ((Location) gp.getValue()).getName() + // : bill.getCashPoint().getLocation().getName()); + + //Text facilityAddressDetails = new Text(gpFacilityAddress != null && gpFacilityAddress.getValue() != null ? gpFacilityAddress.getPropertyValue(): ""); + Paragraph logoSection = null; + if (logoImage != null) { + logoSection = new Paragraph(); + logoSection.setFontSize(14); + logoSection.add(logoImage).add("\n"); + //logoSection.add(facilityName).add("\n"); + logoSection.setTextAlignment(TextAlignment.CENTER); + logoSection.setFont(timesRoman).setBold(); + } + + //Paragraph addressSection = new Paragraph(); + //addressSection.add(facilityAddressDetails).setTextAlignment(TextAlignment.CENTER).setFont(helvetica).setFontSize(12); + + float[] headerColWidth = { 2f, 7f }; + Table receiptHeader = new Table(headerColWidth); + receiptHeader.setWidth(UnitValue.createPercentValue(100f)); + + receiptHeader.addCell(new Paragraph("Date:")).setFontSize(FONT_SIZE_12).setTextAlignment(TextAlignment.LEFT) + .setFont(headerSectionFont); + receiptHeader.addCell(new Paragraph(billDateLabel.getText())).setFontSize(FONT_SIZE_12) + .setTextAlignment(TextAlignment.LEFT).setFont(helvetica); + + receiptHeader.addCell(new Paragraph("Receipt No:")).setFontSize(FONT_SIZE_12).setTextAlignment(TextAlignment.LEFT) + .setFont(headerSectionFont); + receiptHeader.addCell(new Paragraph(bill.getReceiptNumber())).setFontSize(FONT_SIZE_12) + .setTextAlignment(TextAlignment.LEFT).setFont(helvetica); + + receiptHeader.addCell(new Paragraph("Patient:")).setFontSize(FONT_SIZE_12).setTextAlignment(TextAlignment.LEFT) + .setFont(headerSectionFont); + receiptHeader.addCell(new Paragraph(WordUtils.capitalizeFully(fullName))).setFontSize(FONT_SIZE_12) + .setTextAlignment(TextAlignment.LEFT).setFont(helvetica); + + receiptHeader.addCell(new Paragraph("Gender:")).setFontSize(FONT_SIZE_12).setTextAlignment(TextAlignment.LEFT) + .setFont(headerSectionFont); + receiptHeader.addCell(new Paragraph(WordUtils.capitalizeFully(gender))).setFontSize(FONT_SIZE_12) + .setTextAlignment(TextAlignment.LEFT).setFont(helvetica); + + receiptHeader.addCell(new Paragraph("Date of Birth:")).setFontSize(FONT_SIZE_12).setTextAlignment(TextAlignment.LEFT) + .setFont(headerSectionFont); + receiptHeader.addCell(new Paragraph(WordUtils.capitalizeFully(dob))).setFontSize(FONT_SIZE_12) + .setTextAlignment(TextAlignment.LEFT).setFont(helvetica); + + float[] columnWidths = { 1f, 5f, 2f, 2f }; + Table billLineItemstable = new Table(columnWidths); + billLineItemstable.setBorder(Border.NO_BORDER); + billLineItemstable.setWidth(UnitValue.createPercentValue(100f)); + + billLineItemstable.addCell(new Paragraph("Qty").setTextAlignment(TextAlignment.LEFT)).setFontSize(FONT_SIZE_12) + .setTextAlignment(TextAlignment.LEFT); + billLineItemstable.addCell(new Paragraph("Item").setTextAlignment(TextAlignment.LEFT)).setFontSize(FONT_SIZE_12) + .setTextAlignment(TextAlignment.LEFT); + billLineItemstable.addCell(new Paragraph("Price")).setFontSize(FONT_SIZE_12).setTextAlignment(TextAlignment.RIGHT); + billLineItemstable.addCell(new Paragraph("Total")).setFontSize(FONT_SIZE_12).setTextAlignment(TextAlignment.RIGHT); + + for (BillLineItem item : bill.getLineItems()) { + addBillLineItem(item, billLineItemstable, billItemSectionFont); + } + + float[] totalColWidth = { 1f, 5f, 2f, 2f }; + Table totalsSection = new Table(totalColWidth); + totalsSection.setWidth(UnitValue.createPercentValue(100f)); + + totalsSection.addCell(new Paragraph(" ")); + totalsSection.addCell(new Paragraph(" ")); + totalsSection.addCell(new Paragraph("Total")).setFontSize(10).setTextAlignment(TextAlignment.RIGHT) + .setFont(helvetica).setBold(); + totalsSection.addCell(new Paragraph(df.format(bill.getTotal()))).setFontSize(10) + .setTextAlignment(TextAlignment.RIGHT).setFont(helvetica).setBold(); + + setInnerCellBorder(receiptHeader, Border.NO_BORDER); + setInnerCellBorder(billLineItemstable, Border.NO_BORDER); + + float[] paymentColWidth = { 1f, 5f, 2f, 2f }; + Table paymentSection = new Table(paymentColWidth); + paymentSection.setWidth(UnitValue.createPercentValue(100f)); + paymentSection.addCell(new Paragraph(" ")); + paymentSection.addCell(new Paragraph(" ")); + paymentSection.addCell(new Paragraph("Payment").setTextAlignment(TextAlignment.RIGHT).setBold()); + paymentSection.addCell(new Paragraph("")); + // append payment rows + for (Payment payment : bill.getPayments()) { + paymentSection.addCell(new Paragraph(" ")); + paymentSection.addCell(new Paragraph(" ")); + paymentSection.addCell(new Paragraph(payment.getInstanceType().getName()).setTextAlignment(TextAlignment.RIGHT)) + .setFontSize(10).setFont(helvetica); + paymentSection + .addCell(new Paragraph(df.format(payment.getAmountTendered())).setTextAlignment(TextAlignment.RIGHT)) + .setFontSize(10).setFont(helvetica); + } + + float[] amountDueColWidth = { 1f, 5f, 2f, 2f }; + Table amountDueSection = new Table(amountDueColWidth); + amountDueSection.setWidth(UnitValue.createPercentValue(100f)); + + amountDueSection.addCell(new Paragraph(" ")); + amountDueSection.addCell(new Paragraph(" ")); + + amountDueSection.addCell(new Paragraph("Due Amount")).setFontSize(10).setTextAlignment(TextAlignment.RIGHT) + .setFont(helvetica).setBold(); + BigDecimal dueAmount = bill.getTotal().subtract(bill.getTotalPayments()); + if (dueAmount.compareTo(BigDecimal.ZERO) > 0) { + amountDueSection.addCell(new Paragraph(df.format(dueAmount))).setFontSize(10) + .setTextAlignment(TextAlignment.RIGHT).setFont(helvetica).setBold(); + } else { + amountDueSection.addCell(new Paragraph("0.00")).setFontSize(10).setTextAlignment(TextAlignment.RIGHT) + .setFont(helvetica).setBold(); + } + setInnerCellBorder(paymentSection, Border.NO_BORDER); + setInnerCellBorder(amountDueSection, Border.NO_BORDER); + setInnerCellBorder(totalsSection, Border.NO_BORDER); + + try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); + PdfDocument pdfDoc = new PdfDocument(new PdfWriter(bos)); + Document doc = new Document(pdfDoc, new PageSize(thermalPrinterPageSize))) { + doc.setMargins(6, 12, 2, 12); + if (logoSection != null) { + doc.add(logoSection); + } + //doc.add(addressSection); + doc.add(receiptHeader); + doc.add(divider); + doc.add(billLineItemstable); + doc.add(divider); + doc.add(totalsSection); + doc.add(divider); + doc.add(paymentSection); + doc.add(divider); + doc.add(amountDueSection); + doc.add(divider); + doc.add(new Paragraph("You were served by " + bill.getCashier().getName()).setFont(footerSectionFont) + .setFontSize(8).setTextAlignment(TextAlignment.CENTER)); + + return bos.toByteArray(); } - return ReceiptGenerator.createBillReceipt(bill); + catch (IOException e) { + LOG.error("Exception caught while writing PDF to stream", e); + } + + return new byte[0]; + } + + private void setInnerCellBorder(Table table, Border border) { + for (IElement child : table.getChildren()) { + if (child instanceof Cell) { + ((Cell) child).setBorder(border); + } + } + } + + private void addBillLineItem(BillLineItem item, Table table, PdfFont font) { + String itemName = ""; + if (item.getItem() != null) { + itemName = item.getItem().getDrug().getName(); + } else if (item.getBillableService() != null) { + itemName = item.getBillableService().getName(); + } + addFormattedCell(table, item.getQuantity().toString(), font, TextAlignment.LEFT); + addFormattedCell(table, itemName, font, TextAlignment.LEFT); + addFormattedCell(table, df.format(item.getPrice()), font, TextAlignment.RIGHT); + addFormattedCell(table, df.format(item.getTotal()), font, TextAlignment.RIGHT); + } + + private void addFormattedCell(Table table, String cellValue, PdfFont font, TextAlignment alignment) { + table.addCell(new Paragraph(cellValue).setTextAlignment(alignment)).setFontSize(12).setTextAlignment(alignment) + .setBorder(Border.NO_BORDER).setFont(font); } - /** - * {@inheritDoc} - */ @Override - public void purgeBill(Bill bill) { + public List getAll() { + List results = super.getAll(); + removeNullLineItems(results); + return results; + } + + private void removeNullLineItems(List bills) { + if (bills == null || bills.size() == 0) { + return; + } + + for (Bill bill : bills) { + removeNullLineItems(bill); + } + } + + private void removeNullLineItems(Bill bill) { if (bill == null) { - throw new NullPointerException("The bill must be defined."); + return; + } + + // Search for any null line items (due to a bug in 1.7.0) and remove them from the line items + int index = bill.getLineItems().indexOf(null); + while (index >= 0) { + bill.getLineItems().remove(index); + + index = bill.getLineItems().indexOf(null); } - billDAO.purgeBill(bill); } - /** - * {@inheritDoc} - */ @Override - public Bill voidBill(Bill bill, String voidReason) { - if (StringUtils.isBlank(voidReason)) { - throw new IllegalArgumentException("voidReason cannot be null or empty"); - } - return billDAO.saveBill(bill); + public String getVoidPrivilege() { + return PrivilegeConstants.MANAGE_BILLS; + } + + @Override + public String getSavePrivilege() { + return PrivilegeConstants.MANAGE_BILLS; } - /** - * {@inheritDoc} - */ @Override - public Bill unvoidBill(Bill bill) { - return billDAO.saveBill(bill); + public String getPurgePrivilege() { + return PrivilegeConstants.PURGE_BILLS; } + @Override + public String getGetPrivilege() { + return PrivilegeConstants.VIEW_BILLS; + } + + public List searchBill(Patient patient) { + Criteria criteria = getRepository().createCriteria(Bill.class); + + DateTime currentDate = new DateTime(); + DateTime startOfDay = currentDate.withTimeAtStartOfDay(); + + Date startOfDayDate = startOfDay.toDate(); + + DateTime endOfDay = currentDate.plusDays(1); + endOfDay = endOfDay.withTimeAtStartOfDay(); + + Date endOfDayDate = endOfDay.toDate(); + + criteria.add(Restrictions.eq("status", BillStatus.PENDING)); + criteria.add(Restrictions.eq("patient", patient)); + criteria.add(Restrictions.ge("dateCreated", startOfDayDate)); + + criteria.add(Restrictions.lt("dateCreated", endOfDayDate)); + criteria.addOrder(Order.desc("id")); + + return criteria.list(); + } } diff --git a/api/src/main/java/org/openmrs/module/billing/api/impl/BillableItemsServiceImpl.java b/api/src/main/java/org/openmrs/module/billing/api/impl/BillableItemsServiceImpl.java index 20a75948..449065ee 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/impl/BillableItemsServiceImpl.java +++ b/api/src/main/java/org/openmrs/module/billing/api/impl/BillableItemsServiceImpl.java @@ -17,15 +17,15 @@ import org.hibernate.Criteria; import org.openmrs.module.billing.api.IBillableItemsService; -import org.openmrs.module.billing.api.base.entity.impl.BaseMetadataDataServiceImpl; -import org.openmrs.module.billing.api.base.entity.security.IMetadataAuthorizationPrivileges; +import org.openmrs.module.billing.api.base.entity.impl.BaseEntityDataServiceImpl; +import org.openmrs.module.billing.api.base.entity.security.IEntityAuthorizationPrivileges; import org.openmrs.module.billing.api.base.f.Action1; import org.openmrs.module.billing.api.model.BillableService; import org.openmrs.module.billing.api.search.BillableServiceSearch; import org.springframework.transaction.annotation.Transactional; @Transactional -public class BillableItemsServiceImpl extends BaseMetadataDataServiceImpl implements IMetadataAuthorizationPrivileges, IBillableItemsService { +public class BillableItemsServiceImpl extends BaseEntityDataServiceImpl implements IEntityAuthorizationPrivileges, IBillableItemsService { @Override public List findServices(final BillableServiceSearch serviceSearch) { @@ -39,7 +39,7 @@ public void apply(Criteria criteria) { } @Override - protected IMetadataAuthorizationPrivileges getPrivileges() { + protected IEntityAuthorizationPrivileges getPrivileges() { return this; } @@ -49,7 +49,7 @@ protected void validate(BillableService object) { } @Override - public String getRetirePrivilege() { + public String getVoidPrivilege() { return null; } diff --git a/api/src/main/java/org/openmrs/module/billing/api/impl/ICashierItemPriceServiceImpl.java b/api/src/main/java/org/openmrs/module/billing/api/impl/ICashierItemPriceServiceImpl.java index 4738a43f..454f8db3 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/impl/ICashierItemPriceServiceImpl.java +++ b/api/src/main/java/org/openmrs/module/billing/api/impl/ICashierItemPriceServiceImpl.java @@ -14,16 +14,16 @@ package org.openmrs.module.billing.api.impl; import org.openmrs.module.billing.api.ICashierItemPriceService; -import org.openmrs.module.billing.api.base.entity.impl.BaseMetadataDataServiceImpl; -import org.openmrs.module.billing.api.base.entity.security.IMetadataAuthorizationPrivileges; +import org.openmrs.module.billing.api.base.entity.impl.BaseEntityDataServiceImpl; +import org.openmrs.module.billing.api.base.entity.security.IEntityAuthorizationPrivileges; import org.openmrs.module.billing.api.model.CashierItemPrice; import org.springframework.transaction.annotation.Transactional; @Transactional -public class ICashierItemPriceServiceImpl extends BaseMetadataDataServiceImpl implements IMetadataAuthorizationPrivileges, ICashierItemPriceService { +public class ICashierItemPriceServiceImpl extends BaseEntityDataServiceImpl implements IEntityAuthorizationPrivileges, ICashierItemPriceService { @Override - protected IMetadataAuthorizationPrivileges getPrivileges() { + protected IEntityAuthorizationPrivileges getPrivileges() { return this; } @@ -33,7 +33,7 @@ protected void validate(CashierItemPrice object) { } @Override - public String getRetirePrivilege() { + public String getVoidPrivilege() { return null; } diff --git a/api/src/main/java/org/openmrs/module/billing/api/impl/ItemPriceServiceImpl.java b/api/src/main/java/org/openmrs/module/billing/api/impl/ItemPriceServiceImpl.java index b29a2fb3..75243531 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/impl/ItemPriceServiceImpl.java +++ b/api/src/main/java/org/openmrs/module/billing/api/impl/ItemPriceServiceImpl.java @@ -21,20 +21,20 @@ import org.hibernate.criterion.Order; import org.hibernate.criterion.Restrictions; import org.openmrs.module.billing.api.ItemPriceService; -import org.openmrs.module.billing.api.base.entity.impl.BaseMetadataDataServiceImpl; -import org.openmrs.module.billing.api.base.entity.security.IMetadataAuthorizationPrivileges; +import org.openmrs.module.billing.api.base.entity.impl.BaseEntityDataServiceImpl; +import org.openmrs.module.billing.api.base.entity.security.IEntityAuthorizationPrivileges; import org.openmrs.module.billing.api.model.BillableService; import org.openmrs.module.billing.api.model.CashierItemPrice; import org.openmrs.module.stockmanagement.api.model.StockItem; import org.springframework.transaction.annotation.Transactional; @Transactional -public class ItemPriceServiceImpl extends BaseMetadataDataServiceImpl implements IMetadataAuthorizationPrivileges, ItemPriceService { +public class ItemPriceServiceImpl extends BaseEntityDataServiceImpl implements IEntityAuthorizationPrivileges, ItemPriceService { private static final Log LOG = LogFactory.getLog(ItemPriceServiceImpl.class); @Override - protected IMetadataAuthorizationPrivileges getPrivileges() { + protected IEntityAuthorizationPrivileges getPrivileges() { return this; } @@ -44,13 +44,13 @@ protected void validate(CashierItemPrice object) { } @Override - public CashierItemPrice saveBill(CashierItemPrice object) { + public CashierItemPrice save(CashierItemPrice object) { LOG.debug("Processing save Price"); - return super.saveBill(object); + return super.save(object); } @Override - public String getRetirePrivilege() { + public String getVoidPrivilege() { return null; } diff --git a/api/src/main/java/org/openmrs/module/billing/api/model/Bill.java b/api/src/main/java/org/openmrs/module/billing/api/model/Bill.java index 87aa19f5..205253df 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/model/Bill.java +++ b/api/src/main/java/org/openmrs/module/billing/api/model/Bill.java @@ -15,13 +15,12 @@ import java.math.BigDecimal; import java.security.AccessControlException; +import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; -import lombok.Getter; -import lombok.Setter; import org.openmrs.BaseOpenmrsData; import org.openmrs.Patient; import org.openmrs.Provider; @@ -33,11 +32,9 @@ * Model class that represents a list of {@link BillLineItem}s and {@link Payment}s created by a * cashier for a patient. */ -@Getter -@Setter public class Bill extends BaseOpenmrsData { - private static final long serialVersionUID = 0L; + public static final long serialVersionUID = 0L; private Integer billId; @@ -63,10 +60,29 @@ public class Bill extends BaseOpenmrsData { private String adjustmentReason; + public String getAdjustmentReason() { + return adjustmentReason; + } + + public void setAdjustmentReason(String adjustmentReason) { + this.adjustmentReason = adjustmentReason; + } + + public Boolean isReceiptPrinted() { + return receiptPrinted; + } + + public void setReceiptPrinted(Boolean receiptPrinted) { + this.receiptPrinted = receiptPrinted; + } + + public Boolean getReceiptPrinted() { + return receiptPrinted; + } + public BigDecimal getTotal() { BigDecimal total = BigDecimal.ZERO; - List lineItems = getLineItems(); if (lineItems != null) { for (BillLineItem line : lineItems) { if (line != null && !line.getVoided()) { @@ -81,7 +97,6 @@ public BigDecimal getTotal() { public BigDecimal getTotalPayments() { BigDecimal total = BigDecimal.ZERO; - Set payments = getPayments(); if (payments != null) { for (Payment payment : payments) { if (payment != null && !payment.getVoided()) { @@ -93,17 +108,59 @@ public BigDecimal getTotalPayments() { return total; } + public BigDecimal getAmountPaid() { + BigDecimal total = getTotal(); + BigDecimal totalPayments = getTotalPayments(); + + return total.min(totalPayments); + } + @Override public Integer getId() { - return this.getBillId(); + return billId; } @Override public void setId(Integer id) { - this.setBillId(id); + billId = id; + } + + public String getReceiptNumber() { + return receiptNumber; + } + + public void setReceiptNumber(String number) { + this.receiptNumber = number; + } + + public Provider getCashier() { + return cashier; + } + + public void setCashier(Provider cashier) { + this.cashier = cashier; + } + + public Patient getPatient() { + return patient; + } + + public void setPatient(Patient patient) { + this.patient = patient; + } + + public CashPoint getCashPoint() { + return cashPoint; + } + + public void setCashPoint(CashPoint cashPoint) { + this.cashPoint = cashPoint; + } + + public Bill getBillAdjusted() { + return billAdjusted; } - // Custom setter - updates adjusted bill status public void setBillAdjusted(Bill billAdjusted) { this.billAdjusted = billAdjusted; @@ -112,6 +169,32 @@ public void setBillAdjusted(Bill billAdjusted) { } } + public BillStatus getStatus() { + return status; + } + + public void setStatus(BillStatus status) { + this.status = status; + } + + public List getLineItems() { + return lineItems; + } + + public void setLineItems(List lineItems) { + this.lineItems = lineItems; + } + + public BillLineItem addLineItem(StockItem item, CashierItemPrice price, int quantity) { + if (item == null) { + throw new NullPointerException("The item to add must be defined."); + } + if (price == null) { + throw new NullPointerException("The item price must be defined."); + } + return addLineItem(item, price.getPrice(), "", quantity); + } + public BillLineItem addLineItem(StockItem item, BigDecimal price, String priceName, int quantity) { if (item == null) { throw new IllegalArgumentException("The item to add must be defined."); @@ -138,7 +221,7 @@ public void addLineItem(BillLineItem item) { } if (this.lineItems == null) { - this.lineItems = new ArrayList<>(); + this.lineItems = new ArrayList(); } this.lineItems.add(item); @@ -153,13 +236,48 @@ public void removeLineItem(BillLineItem item) { } } + public Set getPayments() { + return payments; + } + + public void setPayments(Set payments) { + this.payments = payments; + } + + public Payment addPayment(PaymentMode mode, Set attributes, BigDecimal amount, + BigDecimal amountTendered) { + if (mode == null) { + throw new NullPointerException("The payment mode must be defined."); + } + if (amount == null) { + throw new NullPointerException(("The payment amount must be defined.")); + } + + Payment payment = new Payment(); + payment.setInstanceType(mode); + payment.setAmount(amount); + payment.setAmountTendered(amountTendered); + + if (attributes != null && attributes.size() > 0) { + payment.setAttributes(attributes); + + for (PaymentAttribute attribute : attributes) { + attribute.setOwner(payment); + } + } + + addPayment(payment); + + return payment; + } + public void addPayment(Payment payment) { if (payment == null) { throw new NullPointerException("The payment to add must be defined."); } if (this.payments == null) { - this.payments = new HashSet<>(); + this.payments = new HashSet(); } this.payments.add(payment); @@ -169,7 +287,7 @@ public void addPayment(Payment payment) { } public void synchronizeBillStatus() { - if (!this.getPayments().isEmpty() && getTotalPayments().compareTo(BigDecimal.ZERO) > 0) { + if (this.getPayments().size() > 0 && getTotalPayments().compareTo(BigDecimal.ZERO) > 0) { boolean billFullySettled = getTotalPayments().compareTo(getTotal()) >= 0; if (billFullySettled) { this.setStatus(BillStatus.PAID); @@ -185,6 +303,14 @@ public void removePayment(Payment payment) { } } + public Set getAdjustedBy() { + return adjustedBy; + } + + public void setAdjustedBy(Set adjustedBy) { + this.adjustedBy = adjustedBy; + } + public void addAdjustedBy(Bill adjustedBill) { checkAuthorizedToAdjust(); if (adjustedBill == null) { @@ -192,31 +318,25 @@ public void addAdjustedBy(Bill adjustedBill) { } if (this.adjustedBy == null) { - this.adjustedBy = new HashSet<>(); + this.adjustedBy = new HashSet(); } adjustedBill.setBillAdjusted(this); this.adjustedBy.add(adjustedBill); } + public void removeAdjustedBy(Bill adjustedBill) { + if (adjustedBill != null && this.adjustedBy != null) { + this.adjustedBy.remove(adjustedBill); + } + } + private void checkAuthorizedToAdjust() { if (!Context.hasPrivilege(PrivilegeConstants.ADJUST_BILLS)) { throw new AccessControlException("Access denied to adjust bill."); } } - /** - * Checks if the bill is in PENDING state. - * - * @return {@code true} if the bill is new (no ID) or is in PENDING state, {@code false} otherwise - */ - public boolean editable() { - // New bills (no ID) are considered pending, existing bills must be in PENDING state - // If we do a partial payment bill is set to POSTED status. We should be able to edit posted status too - return getStatus() == null || this.getId() == null || this.getStatus() == BillStatus.PENDING - || this.getStatus() == BillStatus.POSTED; - } - public void recalculateLineItemOrder() { int orderCounter = 0; for (BillLineItem lineItem : this.getLineItems()) { @@ -224,4 +344,12 @@ public void recalculateLineItemOrder() { } } + public String getLastUpdated() { + SimpleDateFormat ft = Context.getDateTimeFormat(); + String changedStr = (this.getDateChanged() != null) ? ft.format(this.getDateChanged()) : null; + String createdStr = (this.getDateCreated() != null) ? ft.format(this.getDateCreated()) : ""; + String dateString = (changedStr != null) ? changedStr : createdStr; + + return dateString; + } } diff --git a/api/src/main/java/org/openmrs/module/billing/api/model/BillExemption.java b/api/src/main/java/org/openmrs/module/billing/api/model/BillExemption.java deleted file mode 100644 index e4f1168e..00000000 --- a/api/src/main/java/org/openmrs/module/billing/api/model/BillExemption.java +++ /dev/null @@ -1,82 +0,0 @@ -package org.openmrs.module.billing.api.model; - -import org.openmrs.BaseOpenmrsMetadata; -import org.openmrs.Concept; - -import javax.persistence.CascadeType; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.EnumType; -import javax.persistence.Enumerated; -import javax.persistence.FetchType; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.ManyToOne; -import javax.persistence.OneToMany; -import javax.persistence.Table; -import java.util.List; - -@Entity -@Table(name = "bill_exemption") -public class BillExemption extends BaseOpenmrsMetadata { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "exemption_id") - private Integer exemptionId; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "concept_id", nullable = false) - private Concept concept; - - @Enumerated(EnumType.STRING) - @Column(name = "exemption_type", nullable = false) - private ExemptionType exemptionType; - - @OneToMany(mappedBy = "billExemption", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) - private List rules; - - @Override - public Integer getId() { - return exemptionId; - } - - @Override - public void setId(Integer exemptionId) { - this.exemptionId = exemptionId; - } - - public Integer getExemptionId() { - return exemptionId; - } - - public void setExemptionId(Integer exemptionId) { - this.exemptionId = exemptionId; - } - - public Concept getConcept() { - return concept; - } - - public void setConcept(Concept concept) { - this.concept = concept; - } - - public ExemptionType getExemptionType() { - return exemptionType; - } - - public void setExemptionType(ExemptionType exemptionType) { - this.exemptionType = exemptionType; - } - - public List getRules() { - return rules; - } - - public void setRules(List rules) { - this.rules = rules; - } -} diff --git a/api/src/main/java/org/openmrs/module/billing/api/model/BillExemptionRule.java b/api/src/main/java/org/openmrs/module/billing/api/model/BillExemptionRule.java deleted file mode 100644 index d4b8cd32..00000000 --- a/api/src/main/java/org/openmrs/module/billing/api/model/BillExemptionRule.java +++ /dev/null @@ -1,78 +0,0 @@ -package org.openmrs.module.billing.api.model; - -import org.openmrs.BaseOpenmrsData; -import org.openmrs.module.billing.api.evaluator.ScriptType; - -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.EnumType; -import javax.persistence.Enumerated; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.ManyToOne; -import javax.persistence.Table; - -@Entity -@Table(name = "bill_exemption_rule") -public class BillExemptionRule extends BaseOpenmrsData { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "rule_id") - private Integer ruleId; - - @Enumerated(EnumType.STRING) - @Column(name = "script_type", nullable = false) - private ScriptType scriptType; - - @Column(name = "script", nullable = false) - private String script; - - @ManyToOne - @JoinColumn(name = "exemption_id") - private BillExemption billExemption; - - @Override - public Integer getId() { - return getRuleId(); - } - - @Override - public void setId(Integer id) { - setRuleId(id); - } - - public Integer getRuleId() { - return ruleId; - } - - public ScriptType getScriptType() { - return scriptType; - } - - public void setScriptType(ScriptType scriptType) { - this.scriptType = scriptType; - } - - public void setRuleId(Integer ruleId) { - this.ruleId = ruleId; - } - - public String getScript() { - return script; - } - - public void setScript(String script) { - this.script = script; - } - - public BillExemption getBillingExemption() { - return billExemption; - } - - public void setBillingExemption(BillExemption billExemption) { - this.billExemption = billExemption; - } -} diff --git a/api/src/main/java/org/openmrs/module/billing/api/model/BillLineItem.java b/api/src/main/java/org/openmrs/module/billing/api/model/BillLineItem.java index 60cedc09..07c8d84b 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/model/BillLineItem.java +++ b/api/src/main/java/org/openmrs/module/billing/api/model/BillLineItem.java @@ -14,9 +14,8 @@ package org.openmrs.module.billing.api.model; import java.math.BigDecimal; -import java.util.Objects; -import org.openmrs.BaseChangeableOpenmrsData; +import org.openmrs.BaseOpenmrsData; import org.openmrs.Order; import org.openmrs.module.stockmanagement.api.model.StockItem; @@ -24,9 +23,9 @@ * A LineItem represents a line on a {@link Bill} which will bill some quantity of a particular * {@link StockItem}. */ -public class BillLineItem extends BaseChangeableOpenmrsData { +public class BillLineItem extends BaseOpenmrsData { - private static final long serialVersionUID = 0L; + public static final long serialVersionUID = 0L; private int billLineItemId; diff --git a/api/src/main/java/org/openmrs/module/billing/api/model/BillableService.java b/api/src/main/java/org/openmrs/module/billing/api/model/BillableService.java index 10a3b7e8..f8098ca6 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/model/BillableService.java +++ b/api/src/main/java/org/openmrs/module/billing/api/model/BillableService.java @@ -16,12 +16,12 @@ import java.util.ArrayList; import java.util.List; -import org.openmrs.BaseChangeableOpenmrsMetadata; +import org.openmrs.BaseOpenmrsData; import org.openmrs.Concept; import org.openmrs.Location; import org.openmrs.Provider; -public class BillableService extends BaseChangeableOpenmrsMetadata { +public class BillableService extends BaseOpenmrsData { public static final long serialVersionUID = 0L; diff --git a/api/src/main/java/org/openmrs/module/billing/api/model/CashierItemPrice.java b/api/src/main/java/org/openmrs/module/billing/api/model/CashierItemPrice.java index 43656eb4..7023e831 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/model/CashierItemPrice.java +++ b/api/src/main/java/org/openmrs/module/billing/api/model/CashierItemPrice.java @@ -15,10 +15,10 @@ import java.math.BigDecimal; -import org.openmrs.BaseChangeableOpenmrsMetadata; +import org.openmrs.BaseOpenmrsData; import org.openmrs.module.stockmanagement.api.model.StockItem; -public class CashierItemPrice extends BaseChangeableOpenmrsMetadata { +public class CashierItemPrice extends BaseOpenmrsData { public static final long serialVersionUID = 0L; diff --git a/api/src/main/java/org/openmrs/module/billing/api/model/ExemptionType.java b/api/src/main/java/org/openmrs/module/billing/api/model/ExemptionType.java deleted file mode 100644 index 508d3225..00000000 --- a/api/src/main/java/org/openmrs/module/billing/api/model/ExemptionType.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.openmrs.module.billing.api.model; - -public enum ExemptionType { - SERVICE, - COMMODITY, - BOTH -} diff --git a/api/src/main/java/org/openmrs/module/billing/api/search/BillSearch.java b/api/src/main/java/org/openmrs/module/billing/api/search/BillSearch.java index 7e4c79cc..7c64d112 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/search/BillSearch.java +++ b/api/src/main/java/org/openmrs/module/billing/api/search/BillSearch.java @@ -13,36 +13,46 @@ */ package org.openmrs.module.billing.api.search; -import java.util.List; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import org.openmrs.module.billing.api.model.BillStatus; +import org.hibernate.Criteria; +import org.hibernate.criterion.Order; +import org.hibernate.criterion.Restrictions; +import org.openmrs.module.billing.api.base.entity.search.BaseDataTemplateSearch; +import org.openmrs.module.billing.api.model.Bill; /** - * A search criteria holder for {@link org.openmrs.module.billing.api.model.Bill} queries. This - * class holds search parameters that are used by the DAO layer to build queries. Uses Lombok's - * builder pattern for fluent API. + * A search template class for the {@link Bill} model. */ -@Getter -@Setter -@NoArgsConstructor -@AllArgsConstructor -public class BillSearch { - - private String patientUuid; - - private String cashierUuid; - - private String cashPointUuid; +public class BillSearch extends BaseDataTemplateSearch { - private List statuses; + public BillSearch() { + this(new Bill(), false); + } - private String patientName; + public BillSearch(Bill template) { + this(template, false); + } - private Boolean includeVoided = false; + public BillSearch(Bill template, Boolean includeRetired) { + super(template, includeRetired); + } - private Boolean includeVoidedLineItems = false; + @Override + public void updateCriteria(Criteria criteria) { + super.updateCriteria(criteria); + + Bill bill = getTemplate(); + if (bill.getCashier() != null) { + criteria.add(Restrictions.eq("cashier", bill.getCashier())); + } + if (bill.getCashPoint() != null) { + criteria.add(Restrictions.eq("cashPoint", bill.getCashPoint())); + } + if (bill.getPatient() != null) { + criteria.add(Restrictions.eq("patient", bill.getPatient())); + } + if (bill.getStatus() != null) { + criteria.add(Restrictions.eq("status", bill.getStatus())); + } + criteria.addOrder(Order.desc("id")); + } } diff --git a/api/src/main/java/org/openmrs/module/billing/api/search/BillableServiceSearch.java b/api/src/main/java/org/openmrs/module/billing/api/search/BillableServiceSearch.java index 3e08ea09..7891d066 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/search/BillableServiceSearch.java +++ b/api/src/main/java/org/openmrs/module/billing/api/search/BillableServiceSearch.java @@ -16,10 +16,10 @@ import org.hibernate.Criteria; import org.hibernate.criterion.MatchMode; import org.hibernate.criterion.Restrictions; -import org.openmrs.module.billing.api.base.entity.search.BaseMetadataTemplateSearch; +import org.openmrs.module.billing.api.base.entity.search.BaseDataTemplateSearch; import org.openmrs.module.billing.api.model.BillableService; -public class BillableServiceSearch extends BaseMetadataTemplateSearch { +public class BillableServiceSearch extends BaseDataTemplateSearch { public BillableServiceSearch() { this(new BillableService(), false); diff --git a/api/src/main/java/org/openmrs/module/billing/api/util/PrivilegeConstants.java b/api/src/main/java/org/openmrs/module/billing/api/util/PrivilegeConstants.java index 28012c8c..04c0c2fb 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/util/PrivilegeConstants.java +++ b/api/src/main/java/org/openmrs/module/billing/api/util/PrivilegeConstants.java @@ -42,8 +42,6 @@ public class PrivilegeConstants { public static final String PURGE_BILLS = "Purge Cashier Bills"; - public static final String DELETE_BILLS = "Delete Cashier Bills"; - public static final String REFUND_MONEY = "Refund Money"; public static final String REPRINT_RECEIPT = "Reprint Receipt"; diff --git a/api/src/main/java/org/openmrs/module/billing/exemptions/BillingExemptionChecker.java b/api/src/main/java/org/openmrs/module/billing/exemptions/BillingExemptionChecker.java new file mode 100644 index 00000000..58fe6ceb --- /dev/null +++ b/api/src/main/java/org/openmrs/module/billing/exemptions/BillingExemptionChecker.java @@ -0,0 +1,26 @@ +package org.openmrs.module.billing.exemptions; + +import java.util.Set; + +public class BillingExemptionChecker { + + /** + * Checks if a given concept ID is exempted from billing under the provided category. + * + * @param category The category to check (e.g., "services" or "commodities") + * @param key The specific key within the category (e.g., "program:HIV") + * @param conceptId The concept ID to check for exemption + * @return true if the concept ID is exempted, false otherwise + */ + public boolean isExempted(String category, String key, Integer conceptId) { + Set exemptedConcepts; + if (category.equals("services")) { + exemptedConcepts = BillingExemptions.SERVICES.get(key); + } else if (category.equals("commodities")) { + exemptedConcepts = BillingExemptions.COMMODITIES.get(key); + } else { + return false; + } + return exemptedConcepts != null && exemptedConcepts.contains(conceptId); + } +} diff --git a/api/src/main/java/org/openmrs/module/billing/exemptions/BillingExemptions.java b/api/src/main/java/org/openmrs/module/billing/exemptions/BillingExemptions.java new file mode 100644 index 00000000..5045dd11 --- /dev/null +++ b/api/src/main/java/org/openmrs/module/billing/exemptions/BillingExemptions.java @@ -0,0 +1,50 @@ +/* + * The contents of this file are subject to the OpenMRS Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://license.openmrs.org + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the + * License for the specific language governing rights and limitations + * under the License. + * + * Copyright (C) OpenMRS, LLC. All Rights Reserved. + */ +package org.openmrs.module.billing.exemptions; + +import java.util.Map; +import java.util.Set; + +/** + * An object with details of services and commodities exempted from billing. The class variables + * should be populated once on startup and should live in memory during application use. If this + * list grows big, we should think of a separate way to keep this. + */ +public abstract class BillingExemptions { + + /** + * Should contain a list of unique concept ids which are exempted from billing. The convention is to + * have keys that map to a set of concept ids. This is not prescriptive and should be up to an + * implementation to define which convention works A sample payload can look like the below: { + * "services": { "all": [111,112,113], "program:HIV": [211,212,213], "program:TB": [220,220,220], + * "age<5": [311,312,313], "visitAttribute:prisoner": [711,712,713] }, "commodities": { "all": + * [511,512,513], "program:HIV": [611,612,613] } } Please note that the key can be anything, as long + * as the implementation takes care of the evaluation logic There should be a separate logic to + * populate the services and commodities and a separate one to check for exemptions and bill + * appropriately TODO: make the implementation as generic as possible + */ + public static Map> SERVICES; + + public static Map> COMMODITIES; + + public abstract void buildBillingExemptionList(); + + public static void setSERVICES(Map> SERVICES) { + BillingExemptions.SERVICES = SERVICES; + } + + public static void setCOMMODITIES(Map> COMMODITIES) { + BillingExemptions.COMMODITIES = COMMODITIES; + } +} diff --git a/api/src/main/java/org/openmrs/module/billing/exemptions/BillingExemptionsConfig.java b/api/src/main/java/org/openmrs/module/billing/exemptions/BillingExemptionsConfig.java new file mode 100644 index 00000000..91d25de4 --- /dev/null +++ b/api/src/main/java/org/openmrs/module/billing/exemptions/BillingExemptionsConfig.java @@ -0,0 +1,28 @@ +package org.openmrs.module.billing.exemptions; + +import java.util.Map; +import java.util.Set; + +public class BillingExemptionsConfig { + + private Map> services; + + private Map> commodities; + + // Getters and setters + public Map> getServices() { + return services; + } + + public void setServices(Map> services) { + this.services = services; + } + + public Map> getCommodities() { + return commodities; + } + + public void setCommodities(Map> commodities) { + this.commodities = commodities; + } +} diff --git a/api/src/main/java/org/openmrs/module/billing/exemptions/DefaultBillingExemptions.java b/api/src/main/java/org/openmrs/module/billing/exemptions/DefaultBillingExemptions.java new file mode 100644 index 00000000..3d69273a --- /dev/null +++ b/api/src/main/java/org/openmrs/module/billing/exemptions/DefaultBillingExemptions.java @@ -0,0 +1,32 @@ +package org.openmrs.module.billing.exemptions; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import java.io.File; +import java.io.IOException; + +public class DefaultBillingExemptions extends BillingExemptions { + + private static final Log LOG = LogFactory.getLog(DefaultBillingExemptions.class); + + private static final String CONFIG_FILE_PATH = "/billing/exemptions/SampleBillingExemptions.json"; + + @Override + public void buildBillingExemptionList() { + ObjectMapper mapper = new ObjectMapper(); + try { + BillingExemptionsConfig config = mapper.readValue(new File(CONFIG_FILE_PATH), BillingExemptionsConfig.class); + + setSERVICES(config.getServices()); + setCOMMODITIES(config.getCommodities()); + + } + catch (IOException e) { + LOG.error("Failed to load billing exemptions from " + CONFIG_FILE_PATH + ": " + e.getMessage()); + throw new RuntimeException("Unable to load billing exemptions", e); + } + } +} diff --git a/api/src/main/java/org/openmrs/module/billing/exemptions/SampleBillingExemptionBuilder.java b/api/src/main/java/org/openmrs/module/billing/exemptions/SampleBillingExemptionBuilder.java new file mode 100644 index 00000000..1e7b35d0 --- /dev/null +++ b/api/src/main/java/org/openmrs/module/billing/exemptions/SampleBillingExemptionBuilder.java @@ -0,0 +1,126 @@ +package org.openmrs.module.billing.exemptions; + +import org.apache.commons.lang3.StringUtils; +import org.codehaus.jackson.JsonNode; +import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.node.ArrayNode; +import org.codehaus.jackson.node.ObjectNode; +import org.openmrs.GlobalProperty; +import org.openmrs.api.context.Context; +import org.openmrs.module.billing.api.util.CashierModuleConstants; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.*; + +/* + * Builds a list of exemptions from json file +*/ +public class SampleBillingExemptionBuilder extends BillingExemptions { + + private static final Log LOG = LogFactory.getLog(SampleBillingExemptionBuilder.class); + + public SampleBillingExemptionBuilder() { + } + + @Override + public void buildBillingExemptionList() { + GlobalProperty gpConfiguredFilePath = Context.getAdministrationService() + .getGlobalPropertyObject(CashierModuleConstants.BILLING_EXEMPTIONS_CONFIG_FILE_PATH); + if (gpConfiguredFilePath == null || StringUtils.isBlank(gpConfiguredFilePath.getPropertyValue())) { + try { + initializeExemptionsConfig(); + } + catch (Exception e) { + LOG.error("Billing exemptions have not been configured...", e); + } + return; + } + String configurationFilePath = gpConfiguredFilePath.getPropertyValue(); + FileInputStream fileInputStream; + ObjectNode config = null; + try { + fileInputStream = new FileInputStream(configurationFilePath); + } + catch (FileNotFoundException e) { + e.printStackTrace(); + try { + initializeExemptionsConfig(); + } + catch (Exception ex) { + LOG.error("The configuration file for billing exemptions was found, but could not be processed", ex); + } + return; + } + + if (fileInputStream != null) { + ObjectMapper mapper = new ObjectMapper(); + try { + config = mapper.readValue(fileInputStream, ObjectNode.class); + } + catch (IOException e) { + e.printStackTrace(); + try { + initializeExemptionsConfig(); + } + catch (Exception ex) { + LOG.error( + "The configuration file for billing exemptions was found, but could not be understood. Check that the JSON object is well formed", + ex); + } + return; + } + } + + if (config != null) { + ObjectNode configuredServices = (ObjectNode) config.get("services"); + ObjectNode commodities = (ObjectNode) config.get("commodities"); + + if (configuredServices != null) { + Map> exemptedServices = mapConcepts(configuredServices); + BillingExemptions.setSERVICES(exemptedServices); + } + + if (commodities != null) { + Map> exemptedCommodities = mapConcepts(commodities); + BillingExemptions.setCOMMODITIES(exemptedCommodities); + } + } else { + initializeExemptionsConfig(); + } + } + + private Map> mapConcepts(ObjectNode node) { + Map> exemptionList = new HashMap<>(); + if (node != null) { + Iterator> iterator = node.getFields(); + iterator.forEachRemaining(entry -> { + Set conceptSet = new HashSet<>(); + String key = entry.getKey(); + ArrayNode conceptIds = (ArrayNode) entry.getValue(); + if (conceptIds.isArray() && conceptIds.size() > 0) { + for (int i = 0; i < conceptIds.size(); i++) { + try { + conceptSet.add(conceptIds.get(i).getIntValue()); + } + catch (Exception e) { + LOG.error("Error converting concept ID to integer: " + conceptIds.get(i).toString(), e); + } + } + } + if (conceptSet.size() > 0) { + exemptionList.put(key, conceptSet); + } + }); + } + return exemptionList; + } + + private void initializeExemptionsConfig() { + BillingExemptions.setCOMMODITIES(new HashMap<>()); + BillingExemptions.setSERVICES(new HashMap<>()); + } +} diff --git a/api/src/main/java/org/openmrs/module/billing/exemptions/SampleBillingExemptions.json b/api/src/main/java/org/openmrs/module/billing/exemptions/SampleBillingExemptions.json new file mode 100644 index 00000000..678841c1 --- /dev/null +++ b/api/src/main/java/org/openmrs/module/billing/exemptions/SampleBillingExemptions.json @@ -0,0 +1,50 @@ +{ + "services" : { + "all" : [ + { + "id" : "167410AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "description" : "Clinical Consultation" + }, + { + "id" : "160542AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "description" : "Outpatient Department" + } + ], + "program:HIV" : [ + { + "id" : "855e254f-a5db-4760-88b3-26c3d0cdda14", + "description" : "HIV Consultation" + } + ], + "program:TB" : [ + { + "id" : "855e254f-a5db-4760-88b3-26c3d0cdda14", + "description" : "TB Treatment" + } + ], + "age<5" : [ + { + "id" : "160537AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "description" : "Pediatric Consultation" + }, + { + "id" : "1283AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "description" : "Labaratory Orders" + } + ] + }, + "commodities" : { + "all" : [ + { + "id" : "164103AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "description" : "General Commodity" + } + ], + "program:HIV" : [ + { + "id" : "161187AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "description" : "HIV Test Kits" + } + ] + } +} diff --git a/api/src/main/java/org/openmrs/module/billing/util/ReceiptGenerator.java b/api/src/main/java/org/openmrs/module/billing/util/ReceiptGenerator.java deleted file mode 100644 index 8acd19b3..00000000 --- a/api/src/main/java/org/openmrs/module/billing/util/ReceiptGenerator.java +++ /dev/null @@ -1,285 +0,0 @@ -package org.openmrs.module.billing.util; - -import com.itextpdf.io.font.constants.StandardFonts; -import com.itextpdf.io.image.ImageDataFactory; -import com.itextpdf.kernel.font.PdfFont; -import com.itextpdf.kernel.font.PdfFontFactory; -import com.itextpdf.kernel.geom.PageSize; -import com.itextpdf.kernel.geom.Rectangle; -import com.itextpdf.kernel.pdf.PdfDocument; -import com.itextpdf.kernel.pdf.PdfWriter; -import com.itextpdf.layout.Document; -import com.itextpdf.layout.borders.Border; -import com.itextpdf.layout.element.Cell; -import com.itextpdf.layout.element.IElement; -import com.itextpdf.layout.element.Image; -import com.itextpdf.layout.element.Paragraph; -import com.itextpdf.layout.element.Table; -import com.itextpdf.layout.element.Text; -import com.itextpdf.layout.properties.TextAlignment; -import com.itextpdf.layout.properties.UnitValue; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.text.WordUtils; -import org.openmrs.Patient; -import org.openmrs.api.context.Context; -import org.openmrs.module.billing.api.model.Bill; -import org.openmrs.module.billing.api.model.BillLineItem; -import org.openmrs.module.billing.api.model.Payment; -import org.openmrs.util.ConfigUtil; -import org.openmrs.util.OpenmrsClassLoader; -import org.openmrs.util.OpenmrsUtil; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.math.BigDecimal; -import java.net.MalformedURLException; -import java.net.URL; -import java.text.NumberFormat; -import java.time.format.DateTimeFormatter; -import java.time.format.FormatStyle; - -public class ReceiptGenerator { - - private static final Logger LOG = LoggerFactory.getLogger(ReceiptGenerator.class); - - private static final String GP_BILL_LOGO_PATH = "billing.receipt.logoPath"; - - //TODO: Try to clean this up more - public static byte[] createBillReceipt(Bill bill) { - NumberFormat nf = NumberFormat.getCurrencyInstance(Context.getLocale()); - DateTimeFormatter dateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT) - .withLocale(Context.getLocale()); - - Patient patient = bill.getPatient(); - String fullName = patient.getPersonName().getFullName(); - String gender = patient.getGender() != null ? patient.getGender() : ""; - String dob = patient.getBirthdate() != null ? dateFormatter.format(patient.getBirthdate().toInstant()) : ""; - - /** - * https://kb.itextpdf.com/home/it7kb/faq/how-to-set-the-page-size-to-envelope-size-with-landscape-orientation - * page size: 3.5inch length, 1.1 inch height 1mm = 0.0394 inch length = 450mm = 17.7165 inch = - * 127.5588 points height = 300mm = 11.811 inch = 85.0392 points The measurement system in PDF - * doesn't use inches, but user units. By default, 1 user unit = 1 point, and 1 inch = 72 points. - * Thermal printer: 4 x 10 inches paper 4 inches = 4 x 72 = 288 5 inches = 10 x 72 = 720 - */ - int FONT_SIZE_12 = 12; - Rectangle thermalPrinterPageSize = new Rectangle(288, 720); - - PdfFont timesRoman; - PdfFont courierBold; - PdfFont helvetica; - PdfFont helveticaBold; - try { - timesRoman = PdfFontFactory.createFont(StandardFonts.TIMES_ROMAN); - courierBold = PdfFontFactory.createFont(StandardFonts.COURIER_BOLD); - helvetica = PdfFontFactory.createFont(StandardFonts.HELVETICA); - helveticaBold = PdfFontFactory.createFont(StandardFonts.HELVETICA_BOLD); - - } - catch (IOException e) { - throw new RuntimeException(e); - } - - PdfFont headerSectionFont = helveticaBold; - PdfFont billItemSectionFont = helvetica; - PdfFont footerSectionFont = courierBold; - URL logoUrl = null; - - String logoPath = ConfigUtil.getGlobalProperty(GP_BILL_LOGO_PATH); - if (StringUtils.isNotBlank(logoPath)) { - File file = new File(logoPath.trim()); - if (!file.isAbsolute()) { - file = new File(OpenmrsUtil.getApplicationDataDirectory(), logoPath.trim()); - } - - if (file.exists()) { - try { - logoUrl = file.getAbsoluteFile().toURI().toURL(); - } - catch (MalformedURLException e) { - LOG.error("Error Loading file: {}", file.getAbsoluteFile(), e); - } - } - } - - if (logoUrl == null) { - logoUrl = OpenmrsClassLoader.getInstance().getResource("img/openmrs-logo.png"); - } - - Image logoImage = null; - if (logoUrl != null) { - logoImage = new Image(ImageDataFactory.create(logoUrl)); - logoImage.scaleToFit(80, 80); - } - Paragraph divider = new Paragraph("------------------------------------------------------------------"); - Text billDateLabel = new Text(Utils.getSimpleDateFormat("dd-MMM-yyyy HH:mm:ss").format(bill.getDateCreated())); - - Paragraph logoSection = null; - if (logoImage != null) { - logoSection = new Paragraph(); - logoSection.setFontSize(14); - logoSection.add(logoImage).add("\n"); - logoSection.setTextAlignment(TextAlignment.CENTER); - logoSection.setFont(timesRoman).setBold(); - } - - float[] headerColWidth = { 2f, 7f }; - Table receiptHeader = new Table(headerColWidth); - receiptHeader.setWidth(UnitValue.createPercentValue(100f)); - - receiptHeader.addCell(new Paragraph("Date:")).setFontSize(FONT_SIZE_12).setTextAlignment(TextAlignment.LEFT) - .setFont(headerSectionFont); - receiptHeader.addCell(new Paragraph(billDateLabel.getText())).setFontSize(FONT_SIZE_12) - .setTextAlignment(TextAlignment.LEFT).setFont(helvetica); - - receiptHeader.addCell(new Paragraph("Receipt No:")).setFontSize(FONT_SIZE_12).setTextAlignment(TextAlignment.LEFT) - .setFont(headerSectionFont); - receiptHeader.addCell(new Paragraph(bill.getReceiptNumber())).setFontSize(FONT_SIZE_12) - .setTextAlignment(TextAlignment.LEFT).setFont(helvetica); - - receiptHeader.addCell(new Paragraph("Patient:")).setFontSize(FONT_SIZE_12).setTextAlignment(TextAlignment.LEFT) - .setFont(headerSectionFont); - receiptHeader.addCell(new Paragraph(WordUtils.capitalizeFully(fullName))).setFontSize(FONT_SIZE_12) - .setTextAlignment(TextAlignment.LEFT).setFont(helvetica); - - receiptHeader.addCell(new Paragraph("Gender:")).setFontSize(FONT_SIZE_12).setTextAlignment(TextAlignment.LEFT) - .setFont(headerSectionFont); - receiptHeader.addCell(new Paragraph(WordUtils.capitalizeFully(gender))).setFontSize(FONT_SIZE_12) - .setTextAlignment(TextAlignment.LEFT).setFont(helvetica); - - receiptHeader.addCell(new Paragraph("Date of Birth:")).setFontSize(FONT_SIZE_12).setTextAlignment(TextAlignment.LEFT) - .setFont(headerSectionFont); - receiptHeader.addCell(new Paragraph(WordUtils.capitalizeFully(dob))).setFontSize(FONT_SIZE_12) - .setTextAlignment(TextAlignment.LEFT).setFont(helvetica); - - float[] columnWidths = { 1f, 5f, 2f, 2f }; - Table billLineItemstable = new Table(columnWidths); - billLineItemstable.setBorder(Border.NO_BORDER); - billLineItemstable.setWidth(UnitValue.createPercentValue(100f)); - - billLineItemstable.addCell(new Paragraph("Qty").setTextAlignment(TextAlignment.LEFT)).setFontSize(FONT_SIZE_12) - .setTextAlignment(TextAlignment.LEFT); - billLineItemstable.addCell(new Paragraph("Item").setTextAlignment(TextAlignment.LEFT)).setFontSize(FONT_SIZE_12) - .setTextAlignment(TextAlignment.LEFT); - billLineItemstable.addCell(new Paragraph("Price")).setFontSize(FONT_SIZE_12).setTextAlignment(TextAlignment.RIGHT); - billLineItemstable.addCell(new Paragraph("Total")).setFontSize(FONT_SIZE_12).setTextAlignment(TextAlignment.RIGHT); - - for (BillLineItem item : bill.getLineItems()) { - if (item.getVoided()) { - continue; - } - - addBillLineItem(item, billLineItemstable, billItemSectionFont, nf); - } - - float[] totalColWidth = { 1f, 5f, 2f, 2f }; - Table totalsSection = new Table(totalColWidth); - totalsSection.setWidth(UnitValue.createPercentValue(100f)); - - totalsSection.addCell(new Paragraph(" ")); - totalsSection.addCell(new Paragraph(" ")); - totalsSection.addCell(new Paragraph("Total")).setFontSize(10).setTextAlignment(TextAlignment.RIGHT) - .setFont(helvetica).setBold(); - totalsSection.addCell(new Paragraph(nf.format(bill.getTotal()))).setFontSize(10) - .setTextAlignment(TextAlignment.RIGHT).setFont(helvetica).setBold(); - - setInnerCellBorder(receiptHeader, Border.NO_BORDER); - setInnerCellBorder(billLineItemstable, Border.NO_BORDER); - - float[] paymentColWidth = { 1f, 5f, 2f, 2f }; - Table paymentSection = new Table(paymentColWidth); - paymentSection.setWidth(UnitValue.createPercentValue(100f)); - paymentSection.addCell(new Paragraph(" ")); - paymentSection.addCell(new Paragraph(" ")); - paymentSection.addCell(new Paragraph("Payment").setTextAlignment(TextAlignment.RIGHT).setBold()); - paymentSection.addCell(new Paragraph("")); - // append payment rows - for (Payment payment : bill.getPayments()) { - paymentSection.addCell(new Paragraph(" ")); - paymentSection.addCell(new Paragraph(" ")); - paymentSection.addCell(new Paragraph(payment.getInstanceType().getName()).setTextAlignment(TextAlignment.RIGHT)) - .setFontSize(10).setFont(helvetica); - paymentSection - .addCell(new Paragraph(nf.format(payment.getAmountTendered())).setTextAlignment(TextAlignment.RIGHT)) - .setFontSize(10).setFont(helvetica); - } - - float[] amountDueColWidth = { 1f, 5f, 2f, 2f }; - Table amountDueSection = new Table(amountDueColWidth); - amountDueSection.setWidth(UnitValue.createPercentValue(100f)); - - amountDueSection.addCell(new Paragraph(" ")); - amountDueSection.addCell(new Paragraph(" ")); - - amountDueSection.addCell(new Paragraph("Due Amount")).setFontSize(10).setTextAlignment(TextAlignment.RIGHT) - .setFont(helvetica).setBold(); - BigDecimal dueAmount = bill.getTotal().subtract(bill.getTotalPayments()); - if (dueAmount.compareTo(BigDecimal.ZERO) > 0) { - amountDueSection.addCell(new Paragraph(nf.format(dueAmount))).setFontSize(10) - .setTextAlignment(TextAlignment.RIGHT).setFont(helvetica).setBold(); - } else { - amountDueSection.addCell(new Paragraph("0.00")).setFontSize(10).setTextAlignment(TextAlignment.RIGHT) - .setFont(helvetica).setBold(); - } - setInnerCellBorder(paymentSection, Border.NO_BORDER); - setInnerCellBorder(amountDueSection, Border.NO_BORDER); - setInnerCellBorder(totalsSection, Border.NO_BORDER); - - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - try (PdfDocument pdfDoc = new PdfDocument(new PdfWriter(bos)); - Document doc = new Document(pdfDoc, new PageSize(thermalPrinterPageSize))) { - doc.setMargins(6, 12, 2, 12); - if (logoSection != null) { - doc.add(logoSection); - } - //doc.add(addressSection); - doc.add(receiptHeader); - doc.add(divider); - doc.add(billLineItemstable); - doc.add(divider); - doc.add(totalsSection); - doc.add(divider); - doc.add(paymentSection); - doc.add(divider); - doc.add(amountDueSection); - doc.add(divider); - doc.add(new Paragraph("You were served by " + bill.getCashier().getName()).setFont(footerSectionFont) - .setFontSize(8).setTextAlignment(TextAlignment.CENTER)); - } - catch (Exception e) { - LOG.error("Exception caught while writing PDF to stream", e); - return bos.toByteArray(); - } - - return bos.toByteArray(); - } - - private static void setInnerCellBorder(Table table, Border border) { - for (IElement child : table.getChildren()) { - if (child instanceof Cell) { - ((Cell) child).setBorder(border); - } - } - } - - private static void addBillLineItem(BillLineItem item, Table table, PdfFont font, NumberFormat nf) { - String itemName = ""; - if (item.getItem() != null) { - itemName = item.getItem().getDrug().getName(); - } else if (item.getBillableService() != null) { - itemName = item.getBillableService().getName(); - } - addFormattedCell(table, item.getQuantity().toString(), font, TextAlignment.LEFT); - addFormattedCell(table, itemName, font, TextAlignment.LEFT); - addFormattedCell(table, nf.format(item.getPrice()), font, TextAlignment.RIGHT); - addFormattedCell(table, nf.format(item.getTotal()), font, TextAlignment.RIGHT); - } - - private static void addFormattedCell(Table table, String cellValue, PdfFont font, TextAlignment alignment) { - table.addCell(new Paragraph(cellValue).setTextAlignment(alignment)).setFontSize(12).setTextAlignment(alignment) - .setBorder(Border.NO_BORDER).setFont(font); - } -} diff --git a/api/src/main/java/org/openmrs/module/billing/util/Utils.java b/api/src/main/java/org/openmrs/module/billing/util/Utils.java index 1fe88811..ad125e43 100644 --- a/api/src/main/java/org/openmrs/module/billing/util/Utils.java +++ b/api/src/main/java/org/openmrs/module/billing/util/Utils.java @@ -40,9 +40,13 @@ import org.openmrs.Concept; import org.openmrs.Encounter; import org.openmrs.EncounterType; +import org.openmrs.GlobalProperty; +import org.openmrs.Location; +import org.openmrs.LocationAttribute; import org.openmrs.Obs; import org.openmrs.Patient; import org.openmrs.api.context.Context; +import org.openmrs.util.PrivilegeConstants; public class Utils { diff --git a/api/src/main/java/org/openmrs/module/billing/validator/BillValidator.java b/api/src/main/java/org/openmrs/module/billing/validator/BillValidator.java deleted file mode 100644 index e71f9b45..00000000 --- a/api/src/main/java/org/openmrs/module/billing/validator/BillValidator.java +++ /dev/null @@ -1,44 +0,0 @@ -package org.openmrs.module.billing.validator; - -import org.apache.commons.lang3.StringUtils; -import org.openmrs.annotation.Handler; -import org.openmrs.api.context.Context; -import org.openmrs.module.billing.api.BillService; -import org.openmrs.module.billing.api.model.Bill; -import org.springframework.transaction.annotation.Propagation; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.validation.Errors; -import org.springframework.validation.Validator; - -@Handler(supports = { Bill.class }, order = 50) -public class BillValidator implements Validator { - - @Override - public boolean supports(Class clazz) { - return Bill.class.isAssignableFrom(clazz); - } - - @Override - @Transactional(propagation = Propagation.REQUIRES_NEW) - public void validate(Object target, Errors errors) { - if (!(target instanceof Bill)) { - errors.reject("error.general"); - } else { - Bill bill = (Bill) target; - - if (bill.getVoided() && StringUtils.isBlank(bill.getVoidReason())) { - errors.rejectValue("voided", "error.null"); - } - - if (bill.getId() != null) { - Bill existingBill = Context.getService(BillService.class).getBill(bill.getBillId()); - if (existingBill != null && !existingBill.editable()) { - errors.reject("billing.bill.notEditable", - "Bill can only be modified when the bill is in PENDING state. Current status: " - + existingBill.getStatus()); - } - } - } - } - -} diff --git a/api/src/main/resources/Bill.hbm.xml b/api/src/main/resources/Bill.hbm.xml index 18560dc5..597b9c06 100644 --- a/api/src/main/resources/Bill.hbm.xml +++ b/api/src/main/resources/Bill.hbm.xml @@ -29,10 +29,10 @@ - - - - + + + + @@ -61,7 +61,6 @@ - org.openmrs.module.billing.api.model.BillableServiceStatus @@ -75,10 +74,11 @@ - - - - + + + + + @@ -112,12 +112,10 @@ - - diff --git a/api/src/main/resources/moduleApplicationContext.xml b/api/src/main/resources/moduleApplicationContext.xml index 9d90e4c0..5be1c805 100644 --- a/api/src/main/resources/moduleApplicationContext.xml +++ b/api/src/main/resources/moduleApplicationContext.xml @@ -21,6 +21,14 @@
+ + + + org.openmrs.module.billing.api.IBillService + + + + @@ -86,8 +94,6 @@ - - @@ -109,6 +115,16 @@ + + + + + + + + + + @@ -194,65 +210,4 @@ class="org.openmrs.module.billing.api.base.entity.db.hibernate.BaseHibernateRepositoryImpl"> - - - - - org.openmrs.module.billing.api.BillService - - - - - - - - - org.openmrs.module.billing.api.BillExemptionService - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/api/src/test/java/org/openmrs/module/billing/IBillServiceTest.java b/api/src/test/java/org/openmrs/module/billing/IBillServiceTest.java new file mode 100644 index 00000000..81af64e5 --- /dev/null +++ b/api/src/test/java/org/openmrs/module/billing/IBillServiceTest.java @@ -0,0 +1,464 @@ +///* +// * The contents of this file are subject to the OpenMRS Public License +// * Version 1.1 (the "License"); you may not use this file except in +// * compliance with the License. You may obtain a copy of the License at +// * http://license.openmrs.org +// * +// * Software distributed under the License is distributed on an "AS IS" +// * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the +// * License for the specific language governing rights and limitations +// * under the License. +// * +// * Copyright (C) OpenMRS, LLC. All Rights Reserved. +// */ +//package org.openmrs.module.cashier.api; +// +//import java.math.BigDecimal; +//import java.util.Iterator; +//import java.util.List; +//import java.util.Set; +// +////import liquibase.util.StringUtils; +// +//import org.junit.Assert; +//import org.junit.Test; +//import org.openmrs.Patient; +//import org.openmrs.api.PatientService; +//import org.openmrs.api.ProviderService; +//import org.openmrs.api.context.Context; +//import org.openmrs.module.cashier.api.base.PagingInfo; +//import org.openmrs.module.cashier.api.model.Bill; +//import org.openmrs.module.cashier.api.model.BillLineItem; +//import org.openmrs.module.cashier.api.model.BillStatus; +//import org.openmrs.module.cashier.api.model.CashPoint; +//import org.openmrs.module.cashier.api.model.Payment; +//import org.openmrs.module.cashier.api.model.PaymentAttribute; +//import org.openmrs.module.cashier.api.model.PaymentMode; +//import org.openmrs.module.cashier.api.search.BillSearch; +//import org.openmrs.module.cashier.api.base.entity.IEntityDataServiceTest; +//import org.openmrs.module.openhmis.inventory.api.IItemDataService; +//import org.openmrs.module.openhmis.inventory.api.IItemDataServiceTest; +//import org.openmrs.module.openhmis.inventory.api.model.Item; +// +//public abstract class IBillServiceTest extends IEntityDataServiceTest { +// public static final String BILL_DATASET = TestConstants.BASE_DATASET_DIR + "BillTest.xml"; +// +// private ProviderService providerService; +// private PatientService patientService; +// private IItemDataService itemService; +// private IPaymentModeService paymentModeService; +// private IPaymentModeAttributeTypeService paymentModeAttributeTypeService; +// private ICashPointService cashPointService; +// +// @Override +// public void before() throws Exception { +// super.before(); +// +// providerService = Context.getProviderService(); +// patientService = Context.getPatientService(); +// itemService = Context.getService(IItemDataService.class); +// paymentModeService = Context.getService(IPaymentModeService.class); +// paymentModeAttributeTypeService = Context.getService(IPaymentModeAttributeTypeService.class); +// cashPointService = Context.getService(ICashPointService.class); +// +// executeDataSet(IItemDataServiceTest.ITEM_DATASET); +// executeDataSet(IPaymentModeServiceTest.PAYMENT_MODE_DATASET); +// executeDataSet(ICashPointServiceTest.CASH_POINT_DATASET); +// executeDataSet(TestConstants.CORE_DATASET); +// executeDataSet(BILL_DATASET); +// } +// +// @Override +// public Bill createEntity(boolean valid) { +// Bill bill = new Bill(); +// +// if (valid) { +// bill.setCashier(providerService.getProvider(0)); +// bill.setPatient(patientService.getPatient(0)); +// bill.setCashPoint(cashPointService.getById(0)); +// bill.setReceiptNumber("Test 1234"); +// bill.setStatus(BillStatus.PAID); +// } +// +// Item item = itemService.getById(0); +// bill.addLineItem(item, item.getPrices().iterator().next(), 1); +// item = itemService.getById(1); +// bill.addLineItem(item, item.getPrices().iterator().next(), 1); +// +// PaymentMode mode = paymentModeService.getById(0); +// Payment payment = bill.addPayment(mode, null, BigDecimal.valueOf(100), BigDecimal.valueOf(100)); +// payment.addAttribute(paymentModeAttributeTypeService.getById(0), "test"); +// payment.addAttribute(paymentModeAttributeTypeService.getById(1), "test2"); +// payment.addAttribute(paymentModeAttributeTypeService.getById(2), "test3"); +// +// mode = paymentModeService.getById(1); +// bill.addPayment(mode, null, BigDecimal.valueOf(200), BigDecimal.valueOf(200)); +// +// return bill; +// } +// +// @Override +// protected int getTestEntityCount() { +// return 1; +// } +// +// @Override +// protected void updateEntityFields(Bill bill) { +// bill.setCashier(providerService.getProvider(1)); +// bill.setPatient(patientService.getPatient(2)); +// bill.setCashPoint(cashPointService.getById(0)); +// bill.setReceiptNumber(bill.getReceiptNumber() + " updated"); +// bill.setStatus(BillStatus.PENDING); +// +// List lines = bill.getLineItems(); +// if (lines.size() > 0) { +// BillLineItem item = lines.get(0); +// +// item.setPrice(item.getPrice().multiply(BigDecimal.valueOf(2))); +// item.setPriceName(item.getPriceName() + " updated"); +// +// if (lines.size() > 1) { +// item = lines.get(1); +// +// bill.removeLineItem(item); +// } +// } +// +// Item newItem = itemService.getById(2); +// bill.addLineItem(newItem, newItem.getPrices().iterator().next(), 3); +// +// Set payments = bill.getPayments(); +// if (payments.size() > 0) { +// Iterator iterator = payments.iterator(); +// +// Payment payment = iterator.next(); +// payment.setAmount(payment.getAmount().divide(BigDecimal.valueOf(2))); +// +// if (payments.size() > 1) { +// payment = iterator.next(); +// +// bill.removePayment(payment); +// } +// } +// +// bill.addPayment(paymentModeService.getById(2), null, BigDecimal.valueOf(303.11), BigDecimal.valueOf(350.00)); +// } +// +// @Override +// protected void assertEntity(Bill expected, Bill actual) { +// super.assertEntity(expected, actual); +// +// Assert.assertNotNull(expected.getCashier()); +// Assert.assertNotNull(actual.getCashier()); +// Assert.assertEquals(expected.getCashier().getId(), actual.getCashier().getId()); +// Assert.assertNotNull(expected.getPatient()); +// Assert.assertNotNull(actual.getPatient()); +// Assert.assertEquals(expected.getPatient().getId(), actual.getPatient().getId()); +// Assert.assertNotNull(expected.getCashPoint()); +// Assert.assertNotNull(actual.getCashPoint()); +// Assert.assertEquals(expected.getCashPoint().getId(), actual.getCashPoint().getId()); +// +// Assert.assertEquals(expected.getReceiptNumber(), actual.getReceiptNumber()); +// Assert.assertEquals(expected.getStatus(), actual.getStatus()); +// +// if (expected.getLineItems() == null) { +// Assert.assertNull(actual.getLineItems()); +// } else { +// Assert.assertEquals(expected.getLineItems().size(), actual.getLineItems().size()); +// BillLineItem[] expectedItems = new BillLineItem[expected.getLineItems().size()]; +// expected.getLineItems().toArray(expectedItems); +// BillLineItem[] actualItems = new BillLineItem[actual.getLineItems().size()]; +// actual.getLineItems().toArray(actualItems); +// for (int i = 0; i < expected.getLineItems().size(); i++) { +// Assert.assertEquals(expectedItems[i].getId(), actualItems[i].getId()); +// Assert.assertEquals(expectedItems[i].getItem(), actualItems[i].getItem()); +// Assert.assertEquals(expectedItems[i].getPrice(), actualItems[i].getPrice()); +// Assert.assertEquals(expectedItems[i].getPriceName(), actualItems[i].getPriceName()); +// Assert.assertEquals(expectedItems[i].getQuantity(), actualItems[i].getQuantity()); +// Assert.assertEquals(expectedItems[i].getUuid(), actualItems[i].getUuid()); +// } +// } +// +// if (expected.getPayments() == null) { +// Assert.assertNull(actual.getPayments()); +// } else { +// Assert.assertEquals(expected.getPayments().size(), actual.getPayments().size()); +// Payment[] expectedPayments = new Payment[expected.getPayments().size()]; +// expected.getPayments().toArray(expectedPayments); +// Payment[] actualPayments = new Payment[actual.getPayments().size()]; +// actual.getPayments().toArray(actualPayments); +// for (int i = 0; i < expected.getPayments().size(); i++) { +// Assert.assertEquals(expectedPayments[i].getId(), actualPayments[i].getId()); +// Assert.assertEquals(expectedPayments[i].getInstanceType(), actualPayments[i].getInstanceType()); +// Assert.assertEquals(expectedPayments[i].getAmount(), actualPayments[i].getAmount()); +// Assert.assertEquals(expectedPayments[i].getUuid(), actualPayments[i].getUuid()); +// +// if (expectedPayments[i].getAttributes() == null) { +// Assert.assertNull(actualPayments[i].getAttributes()); +// } else { +// Assert.assertEquals(expectedPayments[i].getAttributes().size(), actualPayments[i].getAttributes() +// .size()); +// if (expectedPayments[i].getAttributes().size() > 0) { +// PaymentAttribute[] expectedAttributes = +// new PaymentAttribute[expectedPayments[i].getAttributes().size()]; +// expectedPayments[i].getAttributes().toArray(expectedAttributes); +// PaymentAttribute[] actualAttributes = +// new PaymentAttribute[actualPayments[i].getAttributes().size()]; +// actualPayments[i].getAttributes().toArray(actualAttributes); +// for (int j = 0; j < expectedAttributes.length; j++) { +// Assert.assertEquals(expectedAttributes[j].getId(), actualAttributes[j].getId()); +// Assert.assertEquals(expectedAttributes[j].getValue(), actualAttributes[j].getValue()); +// Assert.assertEquals(expectedAttributes[j].getAttributeType(), +// actualAttributes[j].getAttributeType()); +// Assert.assertEquals(expectedAttributes[j].getUuid(), actualAttributes[j].getUuid()); +// } +// } +// } +// } +// } +// } +// +// /** +// * @verifies throw IllegalArgumentException if the receipt number is null +// * @see IBillService#getBillByReceiptNumber(String) +// */ +// @Test(expected = IllegalArgumentException.class) +// public void getBillByReceiptNumber_shouldThrowIllegalArgumentExceptionIfTheReceiptNumberIsNull() throws Exception { +// service.getBillByReceiptNumber(null); +// } +// +// /** +// * @verifies throw IllegalArgumentException if the receipt number is empty +// * @see IBillService#getBillByReceiptNumber(String) +// */ +// @Test(expected = IllegalArgumentException.class) +// public void getBillByReceiptNumber_shouldThrowIllegalArgumentExceptionIfTheReceiptNumberIsEmpty() throws Exception { +// service.getBillByReceiptNumber(""); +// } +// +// /** +// * @verifies throw IllegalArgumentException if the receipt number is longer than 255 characters +// * @see IBillService#getBillByReceiptNumber(String) +// */ +// @Test(expected = IllegalArgumentException.class) +// public void getBillByReceiptNumber_shouldThrowIllegalArgumentExceptionIfTheReceiptNumberIsLongerThan255Characters() +// throws Exception { +// // service.getBillByReceiptNumber(StringUtils.repeat("A", 256)); +// } +// +// /** +// * @verifies return the bill with the specified reciept number +// * @see IBillService#getBillByReceiptNumber(String) +// */ +// @Test +// public void getBillByReceiptNumber_shouldReturnTheBillWithTheSpecifiedRecieptNumber() throws Exception { +// Bill bill = service.getBillByReceiptNumber("test 1 receipt number"); +// Assert.assertNotNull(bill); +// +// Bill expected = service.getById(0); +// +// assertEntity(expected, bill); +// } +// +// /** +// * @verifies return null if the receipt number is not found +// * @see IBillService#getBillByReceiptNumber(String) +// */ +// @Test +// public void getBillByReceiptNumber_shouldReturnNullIfTheReceiptNumberIsNotFound() throws Exception { +// Bill bill = service.getBillByReceiptNumber("not a valid number"); +// +// Assert.assertNull(bill); +// } +// +// @Test +// public void save_adjustedBill() throws Exception { +// Bill bill = createEntity(true); +// bill.setBillAdjusted(service.getById(0)); +// service.save(bill); +// +// Context.flushSession(); +// +// bill = service.getById(bill.getId()); +// Assert.assertNotNull(bill); +// Assert.assertNotNull(bill.getBillAdjusted()); +// +// Bill adjustedBill = service.getById(bill.getBillAdjusted().getId()); +// Assert.assertNotNull(adjustedBill); +// Assert.assertEquals(BillStatus.ADJUSTED, adjustedBill.getStatus()); +// Assert.assertTrue(adjustedBill.getAdjustedBy().size() > 0); +// +// boolean foundAdjustor = false; +// for (Bill adjustor : adjustedBill.getAdjustedBy()) { +// if (adjustor.getId() == bill.getId()) { +// foundAdjustor = true; +// break; +// } +// } +// +// Assert.assertTrue("Could not find the adjusting bill.", foundAdjustor); +// } +// +// /** +// * @verifies throw NullPointerException if patient is null +// * @see IBillService#getBillsByPatient(org.openmrs.Patient, PagingInfo) +// */ +// @Test(expected = NullPointerException.class) +// public void getBillsByPatient_shouldThrowNullPointerExceptionIfPatientIsNull() throws Exception { +// service.getBillsByPatient(null, null); +// } +// +// /** +// * @verifies return all bills for the specified patient +// * @see IBillService#getBillsByPatient(org.openmrs.Patient, PagingInfo) +// */ +// @Test +// public void getBillsByPatientId_shouldReturnAllBillsForTheSpecifiedPatient() throws Exception { +// Patient patient = patientService.getPatient(0); +// +// List bills = service.getBillsByPatient(patient, null); +// +// Assert.assertNotNull(bills); +// Assert.assertEquals(1, bills.size()); +// assertEntity(service.getById(0), bills.get(0)); +// +// bills = service.getBillsByPatientId(patient.getId(), null); +// Assert.assertNotNull(bills); +// Assert.assertEquals(1, bills.size()); +// assertEntity(service.getById(0), bills.get(0)); +// } +// +// /** +// * @verifies return an empty list if the specified patient has no bills +// * @see IBillService#getBillsByPatient(org.openmrs.Patient, PagingInfo) +// */ +// @Test +// public void getBillsByPatientId_shouldReturnAnEmptyListIfTheSpecifiedPatientHasNoBills() throws Exception { +// Patient patient = patientService.getPatient(1); +// +// List bills = service.getBillsByPatient(patient, null); +// Assert.assertNotNull(bills); +// Assert.assertEquals(0, bills.size()); +// +// bills = service.getBillsByPatientId(1, null); +// Assert.assertNotNull(bills); +// Assert.assertEquals(0, bills.size()); +// } +// +// /** +// * @verifies throw IllegalArgumentException if the patientId is less than zero +// * @see IBillService#getBillsByPatientId(int, PagingInfo) +// */ +// @Test(expected = IllegalArgumentException.class) +// public void getBillsByPatientId_shouldThrowIllegalArgumentExceptionIfThePatientIdIsLessThanZero() throws Exception { +// service.getBillsByPatientId(-1, null); +// } +// +// /** +// * @verifies throw NullPointerException if bill search is null +// * @see IBillService#getBills(BillSearch, PagingInfo) +// */ +// @Test(expected = NullPointerException.class) +// public void getBills_throwNullPointerExceptionIfBillSearchIsNull() throws Exception { +// service.getBills(null, null); +// } +// +// /** +// * @verifies throw NullPointerException if bill search template object is null +// * @see IBillService#getBills(BillSearch, PagingInfo) +// */ +// @Test(expected = NullPointerException.class) +// public void getBills_throwNullPointerExceptionIfBillSearchTemplateObjectIsNull() throws Exception { +// BillSearch search = new BillSearch(); +// search.setTemplate(null); +// service.getBills(search, null); +// } +// +// /** +// * @verifies return an empty list if no bills are found via the search +// * @see IBillService#getBills(BillSearch, PagingInfo) +// */ +// @Test +// public void getBills_returnAnEmptyListIfNoBillsAreFoundViaTheSearch() throws Exception { +// BillSearch billSearch = new BillSearch(); +// Bill bill = new Bill(); +// CashPoint cashPoint = new CashPoint(); +// cashPoint.setId(2); +// bill.setCashPoint(cashPoint); +// billSearch.setTemplate(bill); +// List results = service.getBills(billSearch, null); +// Assert.assertTrue(results.isEmpty()); +// } +// +// /** +// * @verifies return bills filtered by cashier +// * @see IBillService#getBills(BillSearch, PagingInfo) +// */ +// @Test +// public void getBills_returnBillsFilteredByCashier() throws Exception { +// Bill bill = new Bill(); +// bill.setCashier(providerService.getProvider(0)); +// List results = service.getBills(new BillSearch(bill), null); +// Assert.assertEquals(1, results.size()); +// } +// +// /** +// * @verifies return bills filtered by cash point +// * @see IBillService#getBills(BillSearch, PagingInfo) +// */ +// @Test +// public void getBills_returnBillsFilteredByCashPoint() throws Exception { +// Bill bill = new Bill(); +// bill.setCashPoint(cashPointService.getById(0)); +// List results = service.getBills(new BillSearch(bill), null); +// Assert.assertEquals(1, results.size()); +// } +// +// /** +// * @verifies return bills filtered by patient +// * @see IBillService#getBills(BillSearch, PagingInfo) +// */ +// @Test +// public void getBills_returnBillsFilteredByPatient() throws Exception { +// Bill bill = new Bill(); +// bill.setPatient(patientService.getPatient(0)); +// List results = service.getBills(new BillSearch(bill), null); +// Assert.assertEquals(1, results.size()); +// } +// +// /** +// * @verifies return bills filtered by status +// * @see IBillService#getBills(BillSearch, PagingInfo) +// */ +// @Test +// public void getBills_returnBillsFilteredByStatus() throws Exception { +// Bill bill = new Bill(); +// bill.setStatus(BillStatus.POSTED); +// List results = service.getBills(new BillSearch(bill), null); +// Assert.assertEquals(1, results.size()); +// } +// +// /** +// * @verifies return all bills if paging is null +// * @see IBillService#getBills(BillSearch, PagingInfo) +// */ +// @Test +// public void getBills_returnAllBillsIfPagingIsNull() throws Exception { +// List results = service.getBills(new BillSearch(), null); +// Assert.assertEquals(1, results.size()); +// } +// +// /** +// * @verifies return paged bills if paging is specified +// * @see IBillService#getBills(BillSearch, org.openmrs.module.cashier.api.base.PagingInfo) +// */ +// @Test +// public void getBills_returnPagedBillsIfPagingIsSpecified() throws Exception { +// PagingInfo pagingInfo = new PagingInfo(1, 100); +// List results = service.getBills(new BillSearch(), pagingInfo); +// +// Assert.assertNotNull(results); +// Assert.assertEquals(1, results.size()); +// Assert.assertEquals(1, (long)pagingInfo.getTotalRecordCount()); +// } +//} diff --git a/api/src/test/java/org/openmrs/module/billing/ICashPointServiceTest.java b/api/src/test/java/org/openmrs/module/billing/ICashPointServiceTest.java index 75875a62..42fc20d7 100644 --- a/api/src/test/java/org/openmrs/module/billing/ICashPointServiceTest.java +++ b/api/src/test/java/org/openmrs/module/billing/ICashPointServiceTest.java @@ -105,7 +105,7 @@ public void getCashPointsByLocation_shouldNotReturnRetiredCashpointsUnlessSpecif CashPoint cashPoint = service.getById(0); cashPoint.setRetired(true); cashPoint.setRetireReason("reason"); - service.saveBill(cashPoint); + service.save(cashPoint); Location location = Context.getLocationService().getLocation(0); Context.flushSession(); @@ -188,7 +188,7 @@ public void getCashPointsByLocationAndName_shouldNotReturnRetiredCashpointsUnles CashPoint cashPoint = service.getById(0); cashPoint.setRetired(true); cashPoint.setRetireReason("reason"); - service.saveBill(cashPoint); + service.save(cashPoint); Location location = Context.getLocationService().getLocation(0); Context.flushSession(); diff --git a/api/src/test/java/org/openmrs/module/billing/ITimesheetServiceTest.java b/api/src/test/java/org/openmrs/module/billing/ITimesheetServiceTest.java index 54ed7acf..9cd21963 100644 --- a/api/src/test/java/org/openmrs/module/billing/ITimesheetServiceTest.java +++ b/api/src/test/java/org/openmrs/module/billing/ITimesheetServiceTest.java @@ -123,7 +123,7 @@ public void getCurrentTimesheet_shouldReturnTheCurrentTimesheetForTheCashier() t Timesheet timesheet = createEntity(true); timesheet.setClockOut(null); - timesheet = service.saveBill(timesheet); + timesheet = service.save(timesheet); Context.flushSession(); Timesheet current = service.getCurrentTimesheet(timesheet.getCashier()); @@ -161,7 +161,7 @@ public void getCurrentTimesheet_shouldReturnTheMostRecentTimesheetIfTheCashierIs timesheet.setCashier(cashier); timesheet.setClockOut(null); - service.saveBill(timesheet); + service.save(timesheet); Context.flushSession(); Timesheet current = service.getCurrentTimesheet(cashier); diff --git a/api/src/test/java/org/openmrs/module/billing/SequentialReceiptNumberGeneratorTest.java b/api/src/test/java/org/openmrs/module/billing/SequentialReceiptNumberGeneratorTest.java index 1b585fc8..863a7992 100644 --- a/api/src/test/java/org/openmrs/module/billing/SequentialReceiptNumberGeneratorTest.java +++ b/api/src/test/java/org/openmrs/module/billing/SequentialReceiptNumberGeneratorTest.java @@ -14,16 +14,19 @@ package org.openmrs.module.billing; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.powermock.api.mockito.PowerMockito.mockStatic; + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; -import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; -import org.mockito.MockedStatic; +import org.junit.runner.RunWith; import org.openmrs.Provider; import org.openmrs.api.context.Context; import org.openmrs.module.billing.api.ISequentialReceiptNumberGeneratorService; @@ -32,29 +35,32 @@ import org.openmrs.module.billing.api.model.CashPoint; import org.openmrs.module.billing.api.model.SequentialReceiptNumberGeneratorModel; import org.openmrs.patient.impl.LuhnIdentifierValidator; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +@RunWith(PowerMockRunner.class) +@PrepareForTest({ Context.class, SequentialReceiptNumberGenerator.class }) public class SequentialReceiptNumberGeneratorTest { private ISequentialReceiptNumberGeneratorService service; private SequentialReceiptNumberGenerator generator; - private MockedStatic contextMock; + private Calendar calendar; @Before public void before() { - contextMock = mockStatic(Context.class); + mockStatic(Context.class); service = mock(ISequentialReceiptNumberGeneratorService.class); - contextMock.when(() -> Context.getService(ISequentialReceiptNumberGeneratorService.class)).thenReturn(service); + when(Context.getService(ISequentialReceiptNumberGeneratorService.class)).thenReturn(service); + + mockStatic(Calendar.class); + calendar = mock(Calendar.class); + when(Calendar.getInstance()).thenReturn(calendar); generator = new SequentialReceiptNumberGenerator(); } - @After - public void tearDown() { - contextMock.close(); - } - /** * @verifies Create a new receipt number by grouping type * @see SequentialReceiptNumberGenerator#generateNumber(Bill) @@ -137,24 +143,23 @@ public void generateNumber_shouldCreateANewReceiptNumberBySequenceType() throws generator.load(); when(service.reserveNextSequence("")).thenReturn(52013); + Date date = new Date(125, 0, 1, 13, 14, 15); + SimpleDateFormat format = new SimpleDateFormat("yyMMdd"); + when(calendar.getTimeInMillis()).thenReturn(date.getTime()); + number = generator.generateNumber(bill); Assert.assertNotNull(number); - // Should end with the sequence number - Assert.assertTrue(number.endsWith("52013")); - // Should be longer than just the sequence due to date prefix (yyMMdd = 6 chars + 5 digits) - Assert.assertEquals(11, number.length()); + Assert.assertEquals(format.format(date) + "52013", number); - // Test DATE_TIME_COUNTER sequence type model.setSequenceType(SequentialReceiptNumberGenerator.SequenceType.DATE_TIME_COUNTER); generator.load(); when(service.reserveNextSequence("")).thenReturn(15); + format = new SimpleDateFormat("yyMMddHHmmss"); + number = generator.generateNumber(bill); Assert.assertNotNull(number); - // Should end with the sequence number - Assert.assertTrue(number.endsWith("0015")); - // Should be longer than just the sequence due to date-time prefix (yyMMddHHmmss = 12 chars + 4 digits) - Assert.assertEquals(16, number.length()); + Assert.assertEquals(format.format(date) + "0015", number); } /** @@ -181,31 +186,26 @@ public void generateNumber_shouldCreateANewReceiptNumberUsingTheSpecifiedSeparat Assert.assertNotNull(number); Assert.assertEquals("0001", number); - // Test separator with DATE_TIME_COUNTER model.setGroupingType(SequentialReceiptNumberGenerator.GroupingType.CASHIER_AND_CASH_POINT); model.setSequenceType(SequentialReceiptNumberGenerator.SequenceType.DATE_TIME_COUNTER); generator.load(); when(service.reserveNextSequence("P1CP3")).thenReturn(52013); + Date date = new Date(125, 0, 1, 13, 14, 15); + SimpleDateFormat format = new SimpleDateFormat("yyMMddHHmmss"); + when(calendar.getTimeInMillis()).thenReturn(date.getTime()); + number = generator.generateNumber(bill); Assert.assertNotNull(number); - // Should start with grouping and separator - Assert.assertTrue(number.startsWith("P1-CP3-")); - // Should end with the sequence number - Assert.assertTrue(number.endsWith("52013")); + Assert.assertEquals("P1-CP3-" + format.format(date) + "52013", number); model.setIncludeCheckDigit(true); generator.load(); number = generator.generateNumber(bill); Assert.assertNotNull(number); - // Should start with grouping and separator - Assert.assertTrue(number.startsWith("P1-CP3-")); - // Should contain the sequence number before the check digit - Assert.assertTrue(number.contains("52013")); - // Should end with a check digit (single digit after final separator) - String[] parts = number.split("-"); - Assert.assertEquals(1, parts[parts.length - 1].length()); + String expected = "P1-CP3-" + format.format(date) + "52013"; + Assert.assertEquals(expected + "-" + generator.generateCheckDigit(expected), number); } /** diff --git a/api/src/test/java/org/openmrs/module/billing/TestConstants.java b/api/src/test/java/org/openmrs/module/billing/TestConstants.java index 52b84680..9595cdc7 100644 --- a/api/src/test/java/org/openmrs/module/billing/TestConstants.java +++ b/api/src/test/java/org/openmrs/module/billing/TestConstants.java @@ -18,6 +18,4 @@ public class TestConstants { public static final String BASE_DATASET_DIR = "org/openmrs/module/billing/api/include/"; public static final String CORE_DATASET = BASE_DATASET_DIR + "CoreTest.xml"; - - public static final String CORE_DATASET2 = BASE_DATASET_DIR + "CoreTest-2.0.xml"; } diff --git a/api/src/test/java/org/openmrs/module/billing/api/db/hibernate/BillExemptionDAOImplTest.java b/api/src/test/java/org/openmrs/module/billing/api/db/hibernate/BillExemptionDAOImplTest.java deleted file mode 100644 index 8d33147d..00000000 --- a/api/src/test/java/org/openmrs/module/billing/api/db/hibernate/BillExemptionDAOImplTest.java +++ /dev/null @@ -1,296 +0,0 @@ -/* - * The contents of this file are subject to the OpenMRS Public License - * Version 1.1 (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * http://license.openmrs.org - * - * Software distributed under the License is distributed on an "AS IS" - * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the - * License for the specific language governing rights and limitations - * under the License. - * - * Copyright (C) OpenMRS, LLC. All Rights Reserved. - */ - -package org.openmrs.module.billing.api.db.hibernate; - -import org.hibernate.SessionFactory; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.openmrs.BaseOpenmrsMetadata; -import org.openmrs.Concept; -import org.openmrs.api.ConceptService; -import org.openmrs.api.context.Context; -import org.openmrs.module.billing.TestConstants; -import org.openmrs.module.billing.api.db.BillExemptionDAO; -import org.openmrs.module.billing.api.model.BillExemption; -import org.openmrs.module.billing.api.model.BillExemptionRule; -import org.openmrs.module.billing.api.model.ExemptionType; -import org.openmrs.test.jupiter.BaseModuleContextSensitiveTest; -import org.springframework.beans.factory.annotation.Autowired; - -import java.util.Date; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class BillExemptionDAOImplTest extends BaseModuleContextSensitiveTest { - - private static final String EXEMPTION_UUID_1 = "3386610d-d272-43a9-9083-6c2a5272ade9"; - - private BillExemptionDAO dao; - - private ConceptService conceptService; - - @Autowired - private SessionFactory sessionFactory; - - @BeforeEach - public void setup() { - dao = new BillExemptionDAOImpl(sessionFactory); - conceptService = Context.getConceptService(); - executeDataSet(TestConstants.CORE_DATASET2); - executeDataSet(TestConstants.BASE_DATASET_DIR + "BillExemptionTest.xml"); - } - - /** - * @see BillExemptionDAO#getBillingExemptionById(Integer) - */ - @Test - public void getBillingExemptionById_shouldReturnExemptionWithSpecifiedId() { - BillExemption exemption = dao.getBillingExemptionById(1); - - assertNotNull(exemption); - assertEquals(1, exemption.getExemptionId()); - assertEquals("Service Exemption 1", exemption.getName()); - assertEquals(ExemptionType.SERVICE, exemption.getExemptionType()); - assertFalse(exemption.getRetired()); - } - - /** - * @see BillExemptionDAO#getBillingExemptionById(Integer) - */ - @Test - public void getBillingExemptionById_shouldReturnNullForInvalidId() { - BillExemption exemption = dao.getBillingExemptionById(999); - - assertNull(exemption); - } - - /** - * @see BillExemptionDAO#getBillingExemptionByUuid(String) - */ - @Test - public void getBillingExemptionByUuid_shouldReturnExemptionWithSpecifiedUuid() { - BillExemption exemption = dao.getBillingExemptionByUuid(EXEMPTION_UUID_1); - - assertNotNull(exemption); - assertEquals(EXEMPTION_UUID_1, exemption.getUuid()); - assertEquals("Service Exemption 1", exemption.getName()); - assertEquals(ExemptionType.SERVICE, exemption.getExemptionType()); - } - - /** - * @see BillExemptionDAO#getExemptionsByConcept(Concept, ExemptionType, boolean) - */ - @Test - public void getExemptionsByConcept_shouldReturnExemptionsForSpecificConcept() { - Concept concept = conceptService.getConcept(100); - assertNotNull(concept); - - List exemptions = dao.getExemptionsByConcept(concept, null, false); - - assertNotNull(exemptions); - assertEquals(1, exemptions.size()); - assertEquals("Service Exemption 1", exemptions.get(0).getName()); - } - - /** - * @see BillExemptionDAO#getExemptionsByConcept(Concept, ExemptionType, boolean) - */ - @Test - public void getExemptionsByConcept_shouldReturnExemptionsFilteredByExemptionType() { - Concept concept = conceptService.getConcept(100); - assertNotNull(concept); - - List serviceExemptions = dao.getExemptionsByConcept(concept, ExemptionType.SERVICE, false); - - assertNotNull(serviceExemptions); - assertEquals(1, serviceExemptions.size()); - assertEquals(ExemptionType.SERVICE, serviceExemptions.get(0).getExemptionType()); - } - - /** - * @see BillExemptionDAO#getExemptionsByConcept(Concept, ExemptionType, boolean) - */ - @Test - public void getExemptionsByConcept_shouldNotReturnRetiredExemptionsWhenIncludeRetiredIsFalse() { - Concept concept = conceptService.getConcept(103); - assertNotNull(concept); - - List exemptions = dao.getExemptionsByConcept(concept, null, false); - - assertNotNull(exemptions); - assertTrue(exemptions.isEmpty() || exemptions.stream().noneMatch(BaseOpenmrsMetadata::getRetired)); - } - - /** - * @see BillExemptionDAO#getExemptionsByConcept(Concept, ExemptionType, boolean) - */ - @Test - public void getExemptionsByConcept_shouldReturnOnlyRetiredExemptionsWhenIncludeRetiredIsTrue() { - Concept concept = conceptService.getConcept(103); - assertNotNull(concept); - - List exemptions = dao.getExemptionsByConcept(concept, null, true); - - assertNotNull(exemptions); - assertFalse(exemptions.isEmpty()); - assertTrue(exemptions.stream().allMatch(BaseOpenmrsMetadata::getRetired)); - assertEquals("Retired Service Exemption", exemptions.get(0).getName()); - } - - /** - * @see BillExemptionDAO#getExemptionsByItemType(ExemptionType, boolean) - */ - @Test - public void getExemptionsByItemType_shouldReturnAllServiceExemptions() { - List serviceExemptions = dao.getExemptionsByItemType(ExemptionType.SERVICE, false); - - assertNotNull(serviceExemptions); - assertTrue(!serviceExemptions.isEmpty()); - assertTrue( - serviceExemptions.stream().allMatch(e -> e.getExemptionType() == ExemptionType.SERVICE && !e.getRetired())); - } - - /** - * @see BillExemptionDAO#getExemptionsByItemType(ExemptionType, boolean) - */ - @Test - public void getExemptionsByItemType_shouldReturnAllCommodityExemptions() { - List commodityExemptions = dao.getExemptionsByItemType(ExemptionType.COMMODITY, false); - - assertNotNull(commodityExemptions); - assertEquals(1, commodityExemptions.size()); - assertEquals(ExemptionType.COMMODITY, commodityExemptions.get(0).getExemptionType()); - assertEquals("Commodity Exemption 1", commodityExemptions.get(0).getName()); - } - - /** - * @see BillExemptionDAO#getExemptionsByItemType(ExemptionType, boolean) - */ - @Test - public void getExemptionsByItemType_shouldReturnBothTypeExemptions() { - List bothTypeExemptions = dao.getExemptionsByItemType(ExemptionType.BOTH, false); - - assertNotNull(bothTypeExemptions); - assertEquals(1, bothTypeExemptions.size()); - assertEquals(ExemptionType.BOTH, bothTypeExemptions.get(0).getExemptionType()); - assertEquals("Both Type Exemption", bothTypeExemptions.get(0).getName()); - } - - /** - * @see BillExemptionDAO#getExemptionsByItemType(ExemptionType, boolean) - */ - @Test - public void getExemptionsByItemType_shouldReturnAllExemptionsIncludingRetiredWhenIncludeRetiredIsTrue() { - List allExemptions = dao.getExemptionsByItemType(ExemptionType.SERVICE, true); - - assertNotNull(allExemptions); - assertFalse(allExemptions.isEmpty()); - assertTrue(allExemptions.size() >= 2); - assertTrue(allExemptions.stream().anyMatch(BaseOpenmrsMetadata::getRetired)); - assertTrue(allExemptions.stream().anyMatch(e -> !e.getRetired())); - } - - /** - * @see BillExemptionDAO#save(BillExemption) - */ - @Test - public void save_shouldSaveNewBillingExemption() { - Concept concept = conceptService.getConcept(100); - assertNotNull(concept); - - BillExemption newExemption = new BillExemption(); - newExemption.setName("New Test Exemption"); - newExemption.setDescription("Test exemption created by test"); - newExemption.setConcept(concept); - newExemption.setExemptionType(ExemptionType.SERVICE); - newExemption.setCreator(Context.getAuthenticatedUser()); - newExemption.setDateCreated(new Date()); - - BillExemption saved = dao.save(newExemption); - - assertNotNull(saved); - assertNotNull(saved.getExemptionId()); - assertEquals("New Test Exemption", saved.getName()); - assertEquals(ExemptionType.SERVICE, saved.getExemptionType()); - assertEquals(concept.getId(), saved.getConcept().getId()); - } - - /** - * @see BillExemptionDAO#save(BillExemption) - */ - @Test - public void save_shouldUpdateExistingBillingExemption() { - BillExemption exemption = dao.getBillingExemptionById(1); - assertNotNull(exemption); - - String originalName = exemption.getName(); - String newName = "Updated Service Exemption"; - exemption.setName(newName); - - BillExemption updated = dao.save(exemption); - - assertNotNull(updated); - assertEquals(1, updated.getExemptionId()); - assertEquals(newName, updated.getName()); - assertTrue(!originalName.equals(updated.getName())); - } - - /** - * @see BillExemptionDAO#getBillingExemptionById(Integer) - */ - @Test - public void getBillingExemptionById_shouldLoadExemptionWithRules() { - BillExemption exemption = dao.getBillingExemptionById(1); - - assertNotNull(exemption); - assertNotNull(exemption.getRules()); - assertFalse(exemption.getRules().isEmpty()); - - BillExemptionRule rule = exemption.getRules().get(0); - assertNotNull(rule); - assertEquals("patientAge < 5", rule.getScript()); - } - - /** - * @see BillExemptionDAO#getExemptionsByConcept(Concept, ExemptionType, boolean) - */ - @Test - public void getExemptionsByConcept_shouldReturnEmptyListWhenNoMatchingExemptions() { - Concept concept = conceptService.getConcept(100); - assertNotNull(concept); - - List exemptions = dao.getExemptionsByConcept(concept, ExemptionType.COMMODITY, false); - - assertNotNull(exemptions); - assertTrue(exemptions.isEmpty()); - } - - /** - * @see BillExemptionDAO#getExemptionsByItemType(ExemptionType, boolean) - */ - @Test - public void getExemptionsByItemType_shouldReturnAllExemptionsWhenItemTypeIsNull() { - List allExemptions = dao.getExemptionsByItemType(null, false); - - assertNotNull(allExemptions); - assertTrue(allExemptions.size() >= 3); - assertTrue(allExemptions.stream().noneMatch(BaseOpenmrsMetadata::getRetired)); - } -} diff --git a/api/src/test/java/org/openmrs/module/billing/api/evaluator/ExemptionRuleEngineTest.java b/api/src/test/java/org/openmrs/module/billing/api/evaluator/ExemptionRuleEngineTest.java deleted file mode 100644 index d98626db..00000000 --- a/api/src/test/java/org/openmrs/module/billing/api/evaluator/ExemptionRuleEngineTest.java +++ /dev/null @@ -1,289 +0,0 @@ -/* - * The contents of this file are subject to the OpenMRS Public License - * Version 1.1 (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * http://license.openmrs.org - * - * Software distributed under the License is distributed on an "AS IS" - * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the - * License for the specific language governing rights and limitations - * under the License. - * - * Copyright (C) OpenMRS, LLC. All Rights Reserved. - */ - -package org.openmrs.module.billing.api.evaluator; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.openmrs.Concept; -import org.openmrs.Order; -import org.openmrs.Patient; -import org.openmrs.api.ConceptService; -import org.openmrs.api.OrderService; -import org.openmrs.api.PatientService; -import org.openmrs.api.context.Context; -import org.openmrs.module.billing.TestConstants; -import org.openmrs.module.billing.api.BillExemptionService; -import org.openmrs.module.billing.api.evaluator.impl.JSExemptionEvaluator; -import org.openmrs.module.billing.api.model.BillExemption; -import org.openmrs.module.billing.api.model.BillExemptionRule; -import org.openmrs.module.billing.api.model.ExemptionType; -import org.openmrs.test.jupiter.BaseModuleContextSensitiveTest; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class ExemptionRuleEngineTest extends BaseModuleContextSensitiveTest { - - private ExemptionRuleEngine ruleEngine; - - private BillExemptionService billExemptionService; - - private ConceptService conceptService; - - private PatientService patientService; - - private OrderService orderService; - - @BeforeEach - public void setup() { - List evaluators = new ArrayList<>(); - evaluators.add(new JSExemptionEvaluator()); - ruleEngine = new ExemptionRuleEngine(evaluators); - - billExemptionService = Context.getService(BillExemptionService.class); - conceptService = Context.getConceptService(); - patientService = Context.getPatientService(); - orderService = Context.getOrderService(); - - executeDataSet(TestConstants.CORE_DATASET2); - executeDataSet(TestConstants.BASE_DATASET_DIR + "BillExemptionTest.xml"); - } - - /** - * @see ExemptionRuleEngine#evaluateRule(BillExemptionRule, Map) - */ - @Test - public void evaluateRule_shouldEvaluateSimpleRule() { - BillExemptionRule rule = new BillExemptionRule(); - rule.setScriptType(ScriptType.JAVASCRIPT); - rule.setScript("age < 18"); - - Map variables = new HashMap<>(); - variables.put("age", 10); - - boolean result = ruleEngine.evaluateRule(rule, variables); - - assertTrue(result); - } - - /** - * @see ExemptionRuleEngine#evaluateRule(BillExemptionRule, Map) - */ - @Test - public void evaluateRule_shouldReturnFalseWhenRuleFails() { - BillExemptionRule rule = new BillExemptionRule(); - rule.setScriptType(ScriptType.JAVASCRIPT); - rule.setScript("age < 18"); - - Map variables = new HashMap<>(); - variables.put("age", 25); - - boolean result = ruleEngine.evaluateRule(rule, variables); - - assertFalse(result); - } - - /** - * @see ExemptionRuleEngine#isExemptionApplicable(BillExemption, Map) - */ - @Test - public void isExemptionApplicable_shouldReturnTrueWhenAnyRuleMatches() { - BillExemption exemption = billExemptionService.getBillingExemptionById(1); - assertNotNull(exemption); - assertNotNull(exemption.getRules()); - assertFalse(exemption.getRules().isEmpty()); - - Map variables = new HashMap<>(); - variables.put("patientAge", 3); - - boolean result = ruleEngine.isExemptionApplicable(exemption, variables); - - assertTrue(result); - } - - /** - * @see ExemptionRuleEngine#isExemptionApplicable(BillExemption, Map) - */ - @Test - public void isExemptionApplicable_shouldReturnFalseWhenNoRuleMatches() { - BillExemption exemption = billExemptionService.getBillingExemptionById(1); - assertNotNull(exemption); - - Map variables = new HashMap<>(); - variables.put("patientAge", 25); - - boolean result = ruleEngine.isExemptionApplicable(exemption, variables); - - assertFalse(result); - } - - /** - * Integration test mimicking actual order exemption check - */ - @Test - public void checkIfOrderIsExempted_shouldExemptChildrenUnderFive() { - Patient patient = patientService.getPatient(2); - assertNotNull(patient); - - Concept concept = conceptService.getConcept(100); - assertNotNull(concept); - - Order order = new Order(); - order.setPatient(patient); - order.setConcept(concept); - - List exemptions = billExemptionService.getExemptionsByConcept(concept, ExemptionType.SERVICE, false); - - assertNotNull(exemptions); - assertFalse(exemptions.isEmpty()); - - Map variables = buildVariablesMapForOrder(order); - - boolean isExempted = false; - for (BillExemption exemption : exemptions) { - if (ruleEngine.isExemptionApplicable(exemption, variables)) { - isExempted = true; - break; - } - } - - assertTrue(isExempted); - } - - /** - * Integration test with active programs - */ - @Test - public void checkIfOrderIsExempted_shouldCheckActivePrograms() { - Patient patient = patientService.getPatient(2); - assertNotNull(patient); - - Concept concept = conceptService.getConcept(100); - assertNotNull(concept); - - Order order = new Order(); - order.setPatient(patient); - order.setConcept(concept); - - Set activePrograms = new HashSet<>(); - activePrograms.add("HIV Program"); - activePrograms.add("TB Program"); - - Map variables = new HashMap<>(); - variables.put("order", order); - variables.put("patient", patient); - variables.put("patientAge", 4); - variables.put("activePrograms", activePrograms); - - List exemptions = billExemptionService.getExemptionsByConcept(concept, ExemptionType.SERVICE, false); - - boolean isExempted = false; - for (BillExemption exemption : exemptions) { - if (ruleEngine.isExemptionApplicable(exemption, variables)) { - isExempted = true; - break; - } - } - - assertTrue(isExempted); - } - - /** - * Test with elderly patient (>= 65 years) - */ - @Test - public void checkIfOrderIsExempted_shouldExemptElderlyPatients() { - Patient patient = patientService.getPatient(2); - assertNotNull(patient); - - Concept commodityConcept = conceptService.getConcept(102); - assertNotNull(commodityConcept); - - Order order = new Order(); - order.setPatient(patient); - order.setConcept(commodityConcept); - - List exemptions = billExemptionService.getExemptionsByConcept(commodityConcept, - ExemptionType.COMMODITY, false); - - assertNotNull(exemptions); - assertFalse(exemptions.isEmpty()); - - Map variables = new HashMap<>(); - variables.put("order", order); - variables.put("patientAge", 70); - - boolean isExempted = false; - for (BillExemption exemption : exemptions) { - if (ruleEngine.isExemptionApplicable(exemption, variables)) { - isExempted = true; - break; - } - } - - assertTrue(isExempted); - } - - /** - * Test that non-exempted orders return false - */ - @Test - public void checkIfOrderIsExempted_shouldNotExemptNonQualifyingOrders() { - Patient patient = patientService.getPatient(2); - assertNotNull(patient); - - Concept concept = conceptService.getConcept(100); - assertNotNull(concept); - - Order order = new Order(); - order.setPatient(patient); - order.setConcept(concept); - - Map variables = new HashMap<>(); - variables.put("order", order); - variables.put("patientAge", 30); - - List exemptions = billExemptionService.getExemptionsByConcept(concept, ExemptionType.SERVICE, false); - - boolean isExempted = false; - for (BillExemption exemption : exemptions) { - if (ruleEngine.isExemptionApplicable(exemption, variables)) { - isExempted = true; - break; - } - } - - assertFalse(isExempted); - } - - private Map buildVariablesMapForOrder(Order order) { - Map variables = new HashMap<>(); - variables.put("order", order); - variables.put("patientAge", 4); - - Set activePrograms = new HashSet<>(); - variables.put("activePrograms", activePrograms); - - return variables; - } -} diff --git a/api/src/test/java/org/openmrs/module/billing/api/evaluator/impl/JSExemptionEvaluatorTest.java b/api/src/test/java/org/openmrs/module/billing/api/evaluator/impl/JSExemptionEvaluatorTest.java deleted file mode 100644 index 0f1d0ca5..00000000 --- a/api/src/test/java/org/openmrs/module/billing/api/evaluator/impl/JSExemptionEvaluatorTest.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * The contents of this file are subject to the OpenMRS Public License - * Version 1.1 (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * http://license.openmrs.org - * - * Software distributed under the License is distributed on an "AS IS" - * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the - * License for the specific language governing rights and limitations - * under the License. - * - * Copyright (C) OpenMRS, LLC. All Rights Reserved. - */ - -package org.openmrs.module.billing.api.evaluator.impl; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.openmrs.module.billing.api.evaluator.ScriptType; - -import java.util.HashMap; -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class JSExemptionEvaluatorTest { - - private JSExemptionEvaluator evaluator; - - @BeforeEach - public void setup() { - evaluator = new JSExemptionEvaluator(); - } - - /** - * @see JSExemptionEvaluator#getSupportedType() - */ - @Test - public void getSupportedType_shouldReturnJavaScript() { - assertEquals(ScriptType.JAVASCRIPT, evaluator.getSupportedType()); - } - - /** - * @see JSExemptionEvaluator#evaluate(String, Map) - */ - @Test - public void evaluate_shouldReturnTrueForTrueScript() { - boolean result = evaluator.evaluate("true", null); - assertTrue(result); - } - - /** - * @see JSExemptionEvaluator#evaluate(String, Map) - */ - @Test - public void evaluate_shouldReturnFalseForFalseScript() { - boolean result = evaluator.evaluate("false", null); - assertFalse(result); - } - - /** - * @see JSExemptionEvaluator#evaluate(String, Map) - */ - @Test - public void evaluate_shouldEvaluateSimpleComparison() { - Map variables = new HashMap<>(); - variables.put("age", 10); - - boolean result = evaluator.evaluate("age < 18", variables); - assertTrue(result); - } - - /** - * @see JSExemptionEvaluator#evaluate(String, Map) - */ - @Test - public void evaluate_shouldReturnFalseWhenComparisonFails() { - Map variables = new HashMap<>(); - variables.put("age", 25); - - boolean result = evaluator.evaluate("age < 18", variables); - assertFalse(result); - } - - /** - * @see JSExemptionEvaluator#evaluate(String, Map) - */ - @Test - public void evaluate_shouldHandleComplexExpressions() { - Map variables = new HashMap<>(); - variables.put("age", 5); - variables.put("hasInsurance", false); - - boolean result = evaluator.evaluate("age < 18 && !hasInsurance", variables); - assertTrue(result); - } - - /** - * @see JSExemptionEvaluator#evaluate(String, Map) - */ - @Test - public void evaluate_shouldReturnFalseForNullResult() { - boolean result = evaluator.evaluate("null", null); - assertFalse(result); - } - - /** - * @see JSExemptionEvaluator#evaluate(String, Map) - */ - @Test - public void evaluate_shouldThrowExceptionForInvalidScript() { - assertThrows(RuntimeException.class, () -> { - evaluator.evaluate("invalid javascript +++", null); - }); - } -} diff --git a/api/src/test/java/org/openmrs/module/billing/api/impl/BillExemptionServiceImplTest.java b/api/src/test/java/org/openmrs/module/billing/api/impl/BillExemptionServiceImplTest.java deleted file mode 100644 index 8a004e5f..00000000 --- a/api/src/test/java/org/openmrs/module/billing/api/impl/BillExemptionServiceImplTest.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * The contents of this file are subject to the OpenMRS Public License - * Version 1.1 (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * http://license.openmrs.org - * - * Software distributed under the License is distributed on an "AS IS" - * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the - * License for the specific language governing rights and limitations - * under the License. - * - * Copyright (C) OpenMRS, LLC. All Rights Reserved. - */ - -package org.openmrs.module.billing.api.impl; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.openmrs.Concept; -import org.openmrs.api.ConceptService; -import org.openmrs.api.context.Context; -import org.openmrs.module.billing.TestConstants; -import org.openmrs.module.billing.api.BillExemptionService; -import org.openmrs.module.billing.api.model.BillExemption; -import org.openmrs.module.billing.api.model.ExemptionType; -import org.openmrs.test.jupiter.BaseModuleContextSensitiveTest; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; - -public class BillExemptionServiceImplTest extends BaseModuleContextSensitiveTest { - - private static final String EXEMPTION_UUID_1 = "3386610d-d272-43a9-9083-6c2a5272ade9"; - - private BillExemptionService service; - - private ConceptService conceptService; - - @BeforeEach - public void setup() { - service = Context.getService(BillExemptionService.class); - conceptService = Context.getConceptService(); - executeDataSet(TestConstants.CORE_DATASET2); - executeDataSet(TestConstants.BASE_DATASET_DIR + "BillExemptionTest.xml"); - } - - /** - * @see BillExemptionService#getBillingExemptionById(Integer) - */ - @Test - public void getBillingExemptionById_shouldReturnExemptionWithSpecifiedId() { - BillExemption exemption = service.getBillingExemptionById(1); - - assertNotNull(exemption); - assertEquals(1, exemption.getExemptionId()); - assertEquals("Service Exemption 1", exemption.getName()); - } - - /** - * @see BillExemptionService#getBillingExemptionById(Integer) - */ - @Test - public void getBillingExemptionById_shouldReturnNullForInvalidId() { - BillExemption exemption = service.getBillingExemptionById(999); - - assertNull(exemption); - } - - /** - * @see BillExemptionService#getBillingExemptionByUuid(String) - */ - @Test - public void getBillingExemptionByUuid_shouldReturnExemptionWithSpecifiedUuid() { - BillExemption exemption = service.getBillingExemptionByUuid(EXEMPTION_UUID_1); - - assertNotNull(exemption); - assertEquals(EXEMPTION_UUID_1, exemption.getUuid()); - assertEquals("Service Exemption 1", exemption.getName()); - } - - /** - * @see BillExemptionService#getExemptionsByConcept(Concept, ExemptionType, boolean) - */ - @Test - public void getExemptionsByConcept_shouldReturnExemptionsForConcept() { - Concept concept = conceptService.getConcept(100); - - List exemptions = service.getExemptionsByConcept(concept, null, false); - - assertNotNull(exemptions); - assertFalse(exemptions.isEmpty()); - assertEquals("Service Exemption 1", exemptions.get(0).getName()); - } - - /** - * @see BillExemptionService#getExemptionsByItemType(ExemptionType, boolean) - */ - @Test - public void getExemptionsByItemType_shouldReturnServiceExemptions() { - List serviceExemptions = service.getExemptionsByItemType(ExemptionType.SERVICE, false); - - assertNotNull(serviceExemptions); - assertFalse(serviceExemptions.isEmpty()); - assertEquals(ExemptionType.SERVICE, serviceExemptions.get(0).getExemptionType()); - } - - /** - * @see BillExemptionService#getExemptionsByItemType(ExemptionType, boolean) - */ - @Test - public void getExemptionsByItemType_shouldReturnCommodityExemptions() { - List commodityExemptions = service.getExemptionsByItemType(ExemptionType.COMMODITY, false); - - assertNotNull(commodityExemptions); - assertEquals(1, commodityExemptions.size()); - assertEquals(ExemptionType.COMMODITY, commodityExemptions.get(0).getExemptionType()); - } - - /** - * @see BillExemptionService#save(BillExemption) - */ - @Test - public void save_shouldUpdateExemption() { - BillExemption exemption = service.getBillingExemptionById(1); - assertNotNull(exemption); - - exemption.setName("Updated Name"); - BillExemption updated = service.save(exemption); - - assertNotNull(updated); - assertEquals("Updated Name", updated.getName()); - } -} diff --git a/api/src/test/java/org/openmrs/module/billing/api/model/BillTest.java b/api/src/test/java/org/openmrs/module/billing/api/model/BillTest.java deleted file mode 100644 index a043cd2a..00000000 --- a/api/src/test/java/org/openmrs/module/billing/api/model/BillTest.java +++ /dev/null @@ -1,195 +0,0 @@ -package org.openmrs.module.billing.api.model; - -import static org.junit.Assert.assertEquals; - -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.HashSet; - -import org.junit.Test; - -/** - * Test for verifying Bill model methods, particularly getTotalPayments() - */ -public class BillTest { - - @Test - public void getTotalPayments_shouldExcludeVoidedPaymentsFromTotal() { - Bill bill = new Bill(); - bill.setPayments(new HashSet<>()); - - Payment validPayment1 = new Payment(); - validPayment1.setAmountTendered(BigDecimal.valueOf(50)); - validPayment1.setVoided(false); - bill.getPayments().add(validPayment1); - - Payment validPayment2 = new Payment(); - validPayment2.setAmountTendered(BigDecimal.valueOf(30)); - validPayment2.setVoided(false); - bill.getPayments().add(validPayment2); - - Payment voidedPayment1 = new Payment(); - voidedPayment1.setAmountTendered(BigDecimal.valueOf(20)); - voidedPayment1.setVoided(true); - bill.getPayments().add(voidedPayment1); - - Payment voidedPayment2 = new Payment(); - voidedPayment2.setAmountTendered(BigDecimal.valueOf(40)); - voidedPayment2.setVoided(true); - bill.getPayments().add(voidedPayment2); - - assertEquals(BigDecimal.valueOf(80), bill.getTotalPayments()); - } - - @Test - public void getTotalPayments_shouldReturnZeroWhenAllPaymentsAreVoided() { - Bill bill = new Bill(); - bill.setPayments(new HashSet<>()); - - Payment voidedPayment = new Payment(); - voidedPayment.setAmountTendered(BigDecimal.valueOf(100)); - voidedPayment.setVoided(true); - bill.getPayments().add(voidedPayment); - - assertEquals(BigDecimal.ZERO, bill.getTotalPayments()); - } - - @Test - public void getTotal_shouldExcludeVoidedLineItemsFromTotal() { - Bill bill = new Bill(); - bill.setLineItems(new ArrayList<>()); - - BillLineItem lineItem1 = new BillLineItem(); - lineItem1.setPrice(BigDecimal.valueOf(100)); - lineItem1.setQuantity(2); - lineItem1.setVoided(false); - bill.getLineItems().add(lineItem1); - - BillLineItem lineItem2 = new BillLineItem(); - lineItem2.setPrice(BigDecimal.valueOf(50)); - lineItem2.setQuantity(1); - lineItem2.setVoided(false); - bill.getLineItems().add(lineItem2); - - BillLineItem voidedLineItem1 = new BillLineItem(); - voidedLineItem1.setPrice(BigDecimal.valueOf(75)); - voidedLineItem1.setQuantity(3); - voidedLineItem1.setVoided(true); - bill.getLineItems().add(voidedLineItem1); - - BillLineItem voidedLineItem2 = new BillLineItem(); - voidedLineItem2.setPrice(BigDecimal.valueOf(30)); - voidedLineItem2.setQuantity(2); - voidedLineItem2.setVoided(true); - bill.getLineItems().add(voidedLineItem2); - - assertEquals(BigDecimal.valueOf(250), bill.getTotal()); - } - - @Test - public void getTotal_shouldReturnZeroWhenAllLineItemsAreVoided() { - Bill bill = new Bill(); - bill.setLineItems(new ArrayList<>()); - - BillLineItem voidedLineItem = new BillLineItem(); - voidedLineItem.setPrice(BigDecimal.valueOf(100)); - voidedLineItem.setQuantity(5); - voidedLineItem.setVoided(true); - bill.getLineItems().add(voidedLineItem); - - assertEquals(BigDecimal.ZERO, bill.getTotal()); - } - - @Test - public void synchronizeBillStatus_shouldUpdateStatusToPaidWhenFullyPaid() { - Bill bill = new Bill(); - bill.setLineItems(new ArrayList<>()); - bill.setPayments(new HashSet<>()); - - BillLineItem lineItem = new BillLineItem(); - lineItem.setPrice(BigDecimal.valueOf(100)); - lineItem.setQuantity(1); - lineItem.setVoided(false); - bill.getLineItems().add(lineItem); - - Payment payment = new Payment(); - payment.setAmountTendered(BigDecimal.valueOf(100)); - payment.setVoided(false); - bill.getPayments().add(payment); - - bill.synchronizeBillStatus(); - - assertEquals(BillStatus.PAID, bill.getStatus()); - } - - @Test - public void synchronizeBillStatus_shouldUpdateStatusToPostedWhenPartiallyPaid() { - Bill bill = new Bill(); - bill.setLineItems(new ArrayList<>()); - bill.setPayments(new HashSet<>()); - - BillLineItem lineItem = new BillLineItem(); - lineItem.setPrice(BigDecimal.valueOf(100)); - lineItem.setQuantity(1); - lineItem.setVoided(false); - bill.getLineItems().add(lineItem); - - Payment payment = new Payment(); - payment.setAmountTendered(BigDecimal.valueOf(50)); - payment.setVoided(false); - bill.getPayments().add(payment); - - bill.synchronizeBillStatus(); - - assertEquals(BillStatus.POSTED, bill.getStatus()); - } - - @Test - public void synchronizeBillStatus_shouldUpdateStatusToPaidAfterVoidingLineItems() { - Bill bill = new Bill(); - bill.setLineItems(new ArrayList<>()); - bill.setPayments(new HashSet<>()); - - BillLineItem lineItem1 = new BillLineItem(); - lineItem1.setPrice(BigDecimal.valueOf(100)); - lineItem1.setQuantity(1); - lineItem1.setVoided(false); - bill.getLineItems().add(lineItem1); - - BillLineItem lineItem2 = new BillLineItem(); - lineItem2.setPrice(BigDecimal.valueOf(50)); - lineItem2.setQuantity(1); - lineItem2.setVoided(false); - bill.getLineItems().add(lineItem2); - - Payment payment = new Payment(); - payment.setAmountTendered(BigDecimal.valueOf(100)); - payment.setVoided(false); - bill.getPayments().add(payment); - - bill.synchronizeBillStatus(); - assertEquals(BillStatus.POSTED, bill.getStatus()); - - lineItem2.setVoided(true); - - bill.synchronizeBillStatus(); - assertEquals(BillStatus.PAID, bill.getStatus()); - } - - @Test - public void setLineItems_shouldAllowSettingLineItemsOnNewBill() { - Bill bill = new Bill(); - bill.setStatus(BillStatus.PENDING); - - ArrayList lineItems = new ArrayList<>(); - BillLineItem lineItem = new BillLineItem(); - lineItem.setPrice(BigDecimal.valueOf(100)); - lineItem.setQuantity(1); - lineItems.add(lineItem); - - // Should not throw exception for new bill (no ID) - bill.setLineItems(lineItems); - assertEquals(1, bill.getLineItems().size()); - } - -} diff --git a/api/src/test/java/org/openmrs/module/billing/base/entity/IObjectDataServiceTest.java b/api/src/test/java/org/openmrs/module/billing/base/entity/IObjectDataServiceTest.java index d9697201..f4060a55 100644 --- a/api/src/test/java/org/openmrs/module/billing/base/entity/IObjectDataServiceTest.java +++ b/api/src/test/java/org/openmrs/module/billing/base/entity/IObjectDataServiceTest.java @@ -88,8 +88,8 @@ public void before() throws Exception { * @see org.openmrs.module.openhmis.commons.api.entity.IObjectDataService#save(OpenmrsObject) */ @Test(expected = NullPointerException.class) - public void save_Bill_shouldThrowNullPointerExceptionIfTheObjectIsNull() throws Exception { - service.saveBill(null); + public void save_shouldThrowNullPointerExceptionIfTheObjectIsNull() throws Exception { + service.save(null); } /** @@ -97,10 +97,10 @@ public void save_Bill_shouldThrowNullPointerExceptionIfTheObjectIsNull() throws * @see org.openmrs.module.openhmis.commons.api.entity.IObjectDataService#save(OpenmrsObject) */ @Test(expected = APIException.class) - public void save_Bill_shouldValidateTheObjectBeforeSaving() throws Exception { + public void save_shouldValidateTheObjectBeforeSaving() throws Exception { E entity = createEntity(false); - service.saveBill(entity); + service.save(entity); } /** @@ -108,10 +108,10 @@ public void save_Bill_shouldValidateTheObjectBeforeSaving() throws Exception { * @see org.openmrs.module.openhmis.commons.api.entity.IObjectDataService#save(OpenmrsObject) */ @Test - public void save_Bill_shouldReturnSavedObject() throws Exception { + public void save_shouldReturnSavedObject() throws Exception { E entity = createEntity(true); - E result = service.saveBill(entity); + E result = service.save(entity); Context.flushSession(); Assert.assertNotNull(result); @@ -123,13 +123,13 @@ public void save_Bill_shouldReturnSavedObject() throws Exception { * @see org.openmrs.module.openhmis.commons.api.entity.IObjectDataService#save(OpenmrsObject) */ @Test - public void save_Bill_shouldUpdateTheObjectSuccessfully() throws Exception { + public void save_shouldUpdateTheObjectSuccessfully() throws Exception { E entity = service.getById(0); Assert.assertNotNull(entity); updateEntityFields(entity); - service.saveBill(entity); + service.save(entity); Context.flushSession(); E updatedEntity = service.getById(entity.getId()); @@ -141,10 +141,10 @@ public void save_Bill_shouldUpdateTheObjectSuccessfully() throws Exception { * @see org.openmrs.module.openhmis.commons.api.entity.IObjectDataService#save(OpenmrsObject) */ @Test - public void save_Bill_shouldCreateTheObjectSuccessfully() throws Exception { + public void save_shouldCreateTheObjectSuccessfully() throws Exception { E entity = createEntity(true); - entity = service.saveBill(entity); + entity = service.save(entity); Context.flushSession(); E result = service.getById(entity.getId()); @@ -168,7 +168,7 @@ public void purge_shouldThrowNullPointerExceptionIfTheObjectIsNull() throws Exce public void purge_shouldDeleteTheSpecifiedObject() throws Exception { E entity = createEntity(true); - service.saveBill(entity); + service.save(entity); Context.flushSession(); E result = service.getById(entity.getId()); diff --git a/api/src/test/java/org/openmrs/module/billing/db/HibernateBillDAOImplTest.java b/api/src/test/java/org/openmrs/module/billing/db/HibernateBillDAOImplTest.java deleted file mode 100644 index 7f136782..00000000 --- a/api/src/test/java/org/openmrs/module/billing/db/HibernateBillDAOImplTest.java +++ /dev/null @@ -1,243 +0,0 @@ -/* - * The contents of this file are subject to the OpenMRS Public License - * Version 1.1 (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * http://license.openmrs.org - * - * Software distributed under the License is distributed on an "AS IS" - * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the - * License for the specific language governing rights and limitations - * under the License. - * - * Copyright (C) OpenMRS, LLC. All Rights Reserved. - */ -package org.openmrs.module.billing.db; - -import static org.junit.jupiter.api.Assertions.*; - -import java.util.List; -import java.util.UUID; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.openmrs.Patient; -import org.openmrs.api.PatientService; -import org.openmrs.api.ProviderService; -import org.openmrs.api.context.Context; -import org.openmrs.module.billing.TestConstants; -import org.openmrs.module.billing.api.ICashPointService; -import org.openmrs.module.billing.api.base.PagingInfo; -import org.openmrs.module.billing.api.db.BillDAO; -import org.openmrs.module.billing.api.model.Bill; -import org.openmrs.module.billing.api.model.BillStatus; -import org.openmrs.module.billing.api.search.BillSearch; -import org.openmrs.test.jupiter.BaseModuleContextSensitiveTest; - -public class HibernateBillDAOImplTest extends BaseModuleContextSensitiveTest { - - private BillDAO billDAO; - - private PatientService patientService; - - private ProviderService providerService; - - private ICashPointService cashPointService; - - @BeforeEach - public void setup() { - billDAO = Context.getRegisteredComponent("billDAO", BillDAO.class); - patientService = Context.getPatientService(); - providerService = Context.getProviderService(); - cashPointService = Context.getService(ICashPointService.class); - - executeDataSet(TestConstants.CORE_DATASET2); - executeDataSet(TestConstants.BASE_DATASET_DIR + "StockOperationType.xml"); - executeDataSet(TestConstants.BASE_DATASET_DIR + "PaymentModeTest.xml"); - executeDataSet(TestConstants.BASE_DATASET_DIR + "CashPointTest.xml"); - executeDataSet(TestConstants.BASE_DATASET_DIR + "BillTest.xml"); - } - - @Test - public void getBill_shouldReturnBillById() { - Bill bill = billDAO.getBill(0); - assertNotNull(bill); - assertEquals(0, bill.getId()); - } - - @Test - public void getBill_shouldReturnNullIfBillNotFound() { - Bill bill = billDAO.getBill(999); - assertNull(bill); - } - - @Test - public void getBillByUuid_shouldReturnBillByUuid() { - Bill bill = billDAO.getBill(0); - assertNotNull(bill); - String uuid = bill.getUuid(); - - Bill foundBill = billDAO.getBillByUuid(uuid); - assertNotNull(foundBill); - assertEquals(uuid, foundBill.getUuid()); - assertEquals(0, foundBill.getId()); - } - - @Test - public void getBillByUuid_shouldReturnNullIfUuidNotFound() { - Bill bill = billDAO.getBillByUuid("nonexistent-uuid"); - assertNull(bill); - } - - @Test - public void saveBill_shouldCreateNewBill() { - Patient patient = patientService.getPatient(1); - assertNotNull(patient); - - Bill newBill = new Bill(); - newBill.setCashier(providerService.getProvider(0)); - newBill.setPatient(patient); - newBill.setCashPoint(cashPointService.getById(0)); - newBill.setReceiptNumber("TEST-" + UUID.randomUUID()); - newBill.setStatus(BillStatus.PENDING); - - Bill savedBill = billDAO.saveBill(newBill); - Context.flushSession(); - - assertNotNull(savedBill); - assertNotNull(savedBill.getId()); - assertEquals(BillStatus.PENDING, savedBill.getStatus()); - } - - @Test - public void saveBill_shouldUpdateExistingBill() { - Bill existingBill = billDAO.getBill(2); - assertNotNull(existingBill); - assertEquals(BillStatus.PENDING, existingBill.getStatus()); - - String newReceiptNumber = "UPDATED-" + UUID.randomUUID(); - existingBill.setReceiptNumber(newReceiptNumber); - - billDAO.saveBill(existingBill); - Context.flushSession(); - Context.clearSession(); - - Bill updatedBill = billDAO.getBill(2); - assertEquals(newReceiptNumber, updatedBill.getReceiptNumber()); - } - - @Test - public void getBillByReceiptNumber_shouldReturnBillWithMatchingReceiptNumber() { - Bill bill = billDAO.getBillByReceiptNumber("test 1 receipt number"); - assertNotNull(bill); - assertEquals("test 1 receipt number", bill.getReceiptNumber()); - } - - @Test - public void getBillByReceiptNumber_shouldReturnNullIfReceiptNumberNotFound() { - Bill bill = billDAO.getBillByReceiptNumber("nonexistent receipt number"); - assertNull(bill); - } - - @Test - public void getBillsByPatientId_shouldReturnBillsForPatient() { - List bills = billDAO.getBillsByPatientUuid("5631b434-78aa-102b-91a0-001e378eb67e", null); - assertNotNull(bills); - assertFalse(bills.isEmpty()); - assertEquals(1, bills.size()); - } - - @Test - public void getBillsByPatientId_shouldReturnEmptyListWhenPatientHasNoBills() { - List bills = billDAO.getBillsByPatientUuid("abc", null); - assertNotNull(bills); - assertTrue(bills.isEmpty()); - } - - @Test - public void getBillsByPatientId_shouldApplyPagingCorrectly() { - PagingInfo pagingInfo = new PagingInfo(1, 5); - List bills = billDAO.getBillsByPatientUuid("5631b434-78aa-102b-91a0-001e378eb67e", pagingInfo); - - assertNotNull(bills); - assertTrue(bills.size() <= 5); - } - - @Test - public void getBills_shouldReturnAllBillsWhenSearchIsEmpty() { - BillSearch billSearch = new BillSearch(); - List bills = billDAO.getBills(billSearch, null); - - assertNotNull(bills); - assertFalse(bills.isEmpty()); - } - - @Test - public void getBills_shouldFilterByPatientUuid() { - Patient patient = patientService.getPatient(0); - assertNotNull(patient); - - BillSearch billSearch = new BillSearch(); - billSearch.setPatientUuid(patient.getUuid()); - - List bills = billDAO.getBills(billSearch, null); - assertNotNull(bills); - assertFalse(bills.isEmpty()); - - for (Bill bill : bills) { - assertEquals(patient.getUuid(), bill.getPatient().getUuid()); - } - } - - @Test - public void getBills_shouldFilterByCashPointUuid() { - Bill existingBill = billDAO.getBill(0); - assertNotNull(existingBill); - assertNotNull(existingBill.getCashPoint()); - - BillSearch billSearch = new BillSearch(); - billSearch.setCashPointUuid(existingBill.getCashPoint().getUuid()); - - List bills = billDAO.getBills(billSearch, null); - assertNotNull(bills); - assertFalse(bills.isEmpty()); - } - - @Test - public void getBills_shouldExcludeVoidedBillsByDefault() { - BillSearch billSearch = new BillSearch(); - billSearch.setIncludeVoided(false); - - List bills = billDAO.getBills(billSearch, null); - assertNotNull(bills); - - for (Bill bill : bills) { - assertFalse(bill.getVoided()); - } - } - - @Test - public void purgeBill_shouldDeleteBill() { - Patient patient = patientService.getPatient(1); - assertNotNull(patient); - - Bill newBill = new Bill(); - newBill.setCashier(providerService.getProvider(0)); - newBill.setPatient(patient); - newBill.setCashPoint(cashPointService.getById(0)); - newBill.setReceiptNumber("TO-DELETE-" + UUID.randomUUID()); - newBill.setStatus(BillStatus.PENDING); - - Bill savedBill = billDAO.saveBill(newBill); - Context.flushSession(); - - Integer billId = savedBill.getId(); - assertNotNull(billId); - - billDAO.purgeBill(savedBill); - Context.flushSession(); - Context.clearSession(); - - Bill deletedBill = billDAO.getBill(billId); - assertNull(deletedBill); - } -} diff --git a/api/src/test/java/org/openmrs/module/billing/impl/BillServiceImplTest.java b/api/src/test/java/org/openmrs/module/billing/impl/BillServiceImplTest.java index d8004b58..e8e382c1 100644 --- a/api/src/test/java/org/openmrs/module/billing/impl/BillServiceImplTest.java +++ b/api/src/test/java/org/openmrs/module/billing/impl/BillServiceImplTest.java @@ -1,387 +1,121 @@ -/* - * The contents of this file are subject to the OpenMRS Public License - * Version 1.1 (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * http://license.openmrs.org - * - * Software distributed under the License is distributed on an "AS IS" - * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the - * License for the specific language governing rights and limitations - * under the License. - * - * Copyright (C) OpenMRS, LLC. All Rights Reserved. - */ - -package org.openmrs.module.billing.impl; - -import static org.junit.jupiter.api.Assertions.*; - -import java.math.BigDecimal; -import java.util.List; -import java.util.UUID; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.openmrs.Patient; -import org.openmrs.api.PatientService; -import org.openmrs.api.ProviderService; -import org.openmrs.api.context.Context; -import org.openmrs.module.billing.TestConstants; -import org.openmrs.module.billing.api.BillService; -import org.openmrs.module.billing.api.ICashPointService; -import org.openmrs.module.billing.api.base.PagingInfo; -import org.openmrs.module.billing.api.model.Bill; -import org.openmrs.module.billing.api.model.BillLineItem; -import org.openmrs.module.billing.api.model.BillStatus; -import org.openmrs.module.billing.api.search.BillSearch; -import org.openmrs.module.stockmanagement.api.model.StockItem; -import org.openmrs.test.jupiter.BaseModuleContextSensitiveTest; - -public class BillServiceImplTest extends BaseModuleContextSensitiveTest { - - private BillService billService; - - private ProviderService providerService; - - private PatientService patientService; - - private ICashPointService cashPointService; - - @BeforeEach - public void setup() { - billService = Context.getService(BillService.class); - providerService = Context.getProviderService(); - patientService = Context.getPatientService(); - cashPointService = Context.getService(ICashPointService.class); - - executeDataSet(TestConstants.CORE_DATASET2); - executeDataSet(TestConstants.BASE_DATASET_DIR + "StockOperationType.xml"); - executeDataSet(TestConstants.BASE_DATASET_DIR + "PaymentModeTest.xml"); - executeDataSet(TestConstants.BASE_DATASET_DIR + "CashPointTest.xml"); - executeDataSet(TestConstants.BASE_DATASET_DIR + "BillTest.xml"); - } - - /** - * @see org.openmrs.module.billing.api.impl.BillServiceImpl#saveBill(Bill) - */ - @Test - public void save_Bill_shouldThrowNullPointerExceptionIfBillIsNull() { - assertThrows(NullPointerException.class, () -> billService.saveBill(null)); - } - - /** - * @see org.openmrs.module.billing.api.impl.BillServiceImpl#getBillByReceiptNumber(String) - */ - @Test - public void getBillByReceiptNumber_shouldReturnBillWithSpecifiedReceiptNumber() { - Bill bill = billService.getBillByReceiptNumber("test 1 receipt number"); - assertNotNull(bill); - assertEquals("test 1 receipt number", bill.getReceiptNumber()); - } - - /** - * @see org.openmrs.module.billing.api.impl.BillServiceImpl#getBillByReceiptNumber(String) - */ - @Test - public void getBillByReceiptNumber_shouldReturnNullIfReceiptNumberNotFound() { - Bill bill = billService.getBillByReceiptNumber("nonexistent receipt number"); - assertNull(bill); - } - - /** - * @see org.openmrs.module.billing.api.impl.BillServiceImpl#getBillsByPatientUuid(String, - * PagingInfo) - */ - @Test - public void getBillsByPatientUuid_shouldReturnBillsForPatient() { - List bills = billService.getBillsByPatientUuid("5631b434-78aa-102b-91a0-001e378eb67e", null); - assertNotNull(bills); - assertFalse(bills.isEmpty()); - assertEquals(1, bills.size()); - } - - /** - * @see org.openmrs.module.billing.api.impl.BillServiceImpl#getBillsByPatientUuid(String, - * PagingInfo) - */ - @Test - public void getBillsByPatientId_shouldReturnEmptyListWhenPatientHasNoBills() { - List bills = billService.getBillsByPatientUuid("abc", null); - assertNotNull(bills); - assertEquals(0, bills.size()); - } - - /** - * @see org.openmrs.module.billing.api.impl.BillServiceImpl#saveBill(Bill) - */ - @Test - public void save_Bill_shouldCreateNewBillWithNewItem() { - Patient patient = patientService.getPatient(1); - assertNotNull(patient); - - Bill templateBill = billService.getBill(0); - assertNotNull(templateBill); - assertFalse(templateBill.getLineItems().isEmpty()); - - Bill newBill = new Bill(); - newBill.setCashier(providerService.getProvider(0)); - newBill.setPatient(patient); - newBill.setCashPoint(cashPointService.getById(0)); - newBill.setReceiptNumber("TEST-" + UUID.randomUUID()); - newBill.setStatus(BillStatus.PENDING); - - BillLineItem existingItem = templateBill.getLineItems().get(0); - StockItem stockItem = existingItem.getItem(); - - BillLineItem lineItem = newBill.addLineItem(stockItem, BigDecimal.valueOf(150), "New price", 2); - lineItem.setPaymentStatus(BillStatus.PENDING); - lineItem.setUuid(UUID.randomUUID().toString()); - - Bill savedBill = billService.saveBill(newBill); - Context.flushSession(); - - assertNotNull(savedBill); - assertNotNull(savedBill.getId()); - assertEquals(BillStatus.PENDING, savedBill.getStatus()); - assertEquals(1, savedBill.getLineItems().size()); - assertEquals(BigDecimal.valueOf(300), savedBill.getTotal()); - - Bill retrievedBill = billService.getBill(savedBill.getId()); - assertNotNull(retrievedBill); - assertEquals(patient.getId(), retrievedBill.getPatient().getId()); - } - - /** - * @see org.openmrs.module.billing.api.impl.BillServiceImpl#saveBill(Bill) - */ - @Test - public void save_Bill_shouldUpdateExistingBillWithUpdatedBillItem() { - Bill pendingBill = billService.getBill(2); - assertNotNull(pendingBill); - assertEquals(BillStatus.PENDING, pendingBill.getStatus()); - assertFalse(pendingBill.getLineItems().isEmpty()); - - BillLineItem firstItem = pendingBill.getLineItems().get(0); - BigDecimal updatedPrice = firstItem.getPrice().add(BigDecimal.TEN); - firstItem.setPrice(updatedPrice); - - billService.saveBill(pendingBill); - Context.flushSession(); - Context.clearSession(); - - Bill updatedBill = billService.getBill(2); - - assertEquals(pendingBill, updatedBill); - assertEquals(updatedPrice, updatedBill.getLineItems().get(0).getPrice()); - } - - /** - * @see org.openmrs.module.billing.api.impl.BillServiceImpl#getBill(Integer) - */ - @Test - public void getById_shouldReturnBillWithSpecifiedId() { - Bill bill = billService.getBill(1); - assertNotNull(bill); - assertEquals(1, bill.getId()); - } - - /** - * @see org.openmrs.module.billing.api.impl.BillServiceImpl#getBill(Integer) - */ - @Test - public void getById_shouldRemoveNullLineItems() { - Bill bill = billService.getBill(1); - assertNotNull(bill); - if (bill.getLineItems() != null) { - for (Object item : bill.getLineItems()) { - assertNotNull(item, "Line items should not contain null values"); - } - } - } - - /** - * @see org.openmrs.module.billing.api.impl.BillServiceImpl#saveBill(Bill) - */ - @Test - public void save_Bill_shouldAllowAddingLineItemsToPendingBill() { - // Get the PENDING bill from test data (bill_id=2) - Bill pendingBill = billService.getBill(2); - assertNotNull(pendingBill); - assertEquals(BillStatus.PENDING, pendingBill.getStatus()); - - // Add a new line item - BillLineItem newLineItem = new BillLineItem(); - newLineItem.setPrice(BigDecimal.valueOf(25.50)); - newLineItem.setQuantity(2); - newLineItem.setPaymentStatus(BillStatus.PENDING); - newLineItem.setLineItemOrder(pendingBill.getLineItems().size()); - pendingBill.addLineItem(newLineItem); - - // Should not throw exception - Bill savedBill = billService.saveBill(pendingBill); - assertNotNull(savedBill); - assertFalse(savedBill.getLineItems().isEmpty()); - } - - /** - * @see org.openmrs.module.billing.api.impl.BillServiceImpl#saveBill(Bill) - */ - @Test - public void save_Bill_shouldThrowExceptionWhenAddingLineItemsToPaidBill() { - // Get the PAID bill from test data (bill_id=1) - Bill paidBill = billService.getBill(1); - assertNotNull(paidBill); - assertEquals(BillStatus.PAID, paidBill.getStatus()); - - // Try to add a new line item - BillLineItem newLineItem = new BillLineItem(); - newLineItem.setPrice(BigDecimal.valueOf(25.50)); - newLineItem.setQuantity(2); - paidBill.addLineItem(newLineItem); - // Should throw exception - - assertThrows(IllegalArgumentException.class, () -> billService.saveBill(paidBill)); - } - - /** - * @see org.openmrs.module.billing.api.impl.BillServiceImpl#saveBill(Bill) - */ - @Test - public void save_Bill_shouldAllowRemovingLineItemsFromPendingBill() { - // Get the PENDING bill from test data (bill_id=2) - Bill pendingBill = billService.getBill(2); - assertNotNull(pendingBill); - assertEquals(BillStatus.PENDING, pendingBill.getStatus()); - - int originalSize = pendingBill.getLineItems().size(); - assertTrue(originalSize > 0); - - // Remove a line item - BillLineItem itemToRemove = pendingBill.getLineItems().get(0); - pendingBill.removeLineItem(itemToRemove); - - // Should not throw exception - Bill savedBill = billService.saveBill(pendingBill); - assertNotNull(savedBill); - assertTrue(savedBill.getLineItems().size() < originalSize); - } - - /** - * @see org.openmrs.module.billing.api.impl.BillServiceImpl#saveBill(Bill) - */ - @Test - public void save_Bill_shouldThrowExceptionWhenRemovingLineItemsFromPaidBill() { - // Get the POSTED bill from test data (bill_id=1) - Bill postedBill = billService.getBill(1); - assertNotNull(postedBill); - assertEquals(BillStatus.PAID, postedBill.getStatus()); - - BillLineItem itemToRemove = postedBill.getLineItems().get(0); - postedBill.removeLineItem(itemToRemove); - - // Should throw exception - assertThrows(IllegalArgumentException.class, () -> billService.saveBill(postedBill)); - } - - @Test - public void save_Bill_shouldNotThrowExceptionForPendingBill() { - Bill pendingBill = billService.getBill(2); - assertNotNull(pendingBill); - assertEquals(BillStatus.PENDING, pendingBill.getStatus()); - pendingBill.setReceiptNumber("ABV"); - assertDoesNotThrow(() -> billService.saveBill(pendingBill)); - } - - @Test - public void save_Bill_shouldThrowIllegalStateExceptionForPostedBill() { - Bill postedBill = billService.getBill(0); - assertNotNull(postedBill); - assertEquals(BillStatus.POSTED, postedBill.getStatus()); - - postedBill.setReceiptNumber("ABV"); - assertThrows(IllegalArgumentException.class, () -> billService.saveBill(postedBill)); - } - - /** - * @see org.openmrs.module.billing.api.impl.BillServiceImpl#getBillByUuid(String) - */ - @Test - public void getBillByUuid_shouldReturnBillWithSpecifiedUuid() { - Bill bill = billService.getBill(0); - assertNotNull(bill); - String uuid = bill.getUuid(); - - Bill foundBill = billService.getBillByUuid(uuid); - assertNotNull(foundBill); - assertEquals(uuid, foundBill.getUuid()); - assertEquals(0, foundBill.getId()); - } - - /** - * @see org.openmrs.module.billing.api.impl.BillServiceImpl#getBillByUuid(String) - */ - @Test - public void getBillByUuid_shouldReturnNullIfUuidNotFound() { - Bill bill = billService.getBillByUuid("nonexistent-uuid"); - assertNull(bill); - } - - /** - * @see org.openmrs.module.billing.api.impl.BillServiceImpl#getBills(BillSearch, PagingInfo) - */ - @Test - public void getBills_shouldReturnAllBillsWhenSearchIsEmpty() { - BillSearch billSearch = new BillSearch(); - List bills = billService.getBills(billSearch, null); - - assertNotNull(bills); - assertFalse(bills.isEmpty()); - } - - /** - * @see org.openmrs.module.billing.api.impl.BillServiceImpl#getBills(BillSearch, PagingInfo) - */ - @Test - public void getBills_shouldFilterByPatientUuid() { - Patient patient = patientService.getPatient(0); - assertNotNull(patient); - - BillSearch billSearch = new BillSearch(); - billSearch.setPatientUuid(patient.getUuid()); - - List bills = billService.getBills(billSearch, null); - assertNotNull(bills); - assertFalse(bills.isEmpty()); - - for (Bill bill : bills) { - assertEquals(patient.getUuid(), bill.getPatient().getUuid()); - } - } - - /** - * @see org.openmrs.module.billing.api.impl.BillServiceImpl#getBills(BillSearch, PagingInfo) - */ - @Test - public void getBills_shouldReturnEmptyListWhenSearchReturnsNoResults() { - BillSearch billSearch = new BillSearch(); - billSearch.setPatientUuid("nonexistent-uuid"); - - List bills = billService.getBills(billSearch, null); - assertNotNull(bills); - assertTrue(bills.isEmpty()); - } - - /** - * @see org.openmrs.module.billing.api.impl.BillServiceImpl#getBills(BillSearch, PagingInfo) - */ - @Test - public void getBills_shouldApplyPagingCorrectly() { - BillSearch billSearch = new BillSearch(); - PagingInfo pagingInfo = new PagingInfo(1, 2); - - List bills = billService.getBills(billSearch, pagingInfo); - assertNotNull(bills); - assertTrue(bills.size() <= 2); - assertNotNull(pagingInfo.getTotalRecordCount()); - } -} +///* +// * The contents of this file are subject to the OpenMRS Public License +// * Version 1.1 (the "License"); you may not use this file except in +// * compliance with the License. You may obtain a copy of the License at +// * http://license.openmrs.org +// * +// * Software distributed under the License is distributed on an "AS IS" +// * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the +// * License for the specific language governing rights and limitations +// * under the License. +// * +// * Copyright (C) OpenMRS, LLC. All Rights Reserved. +// */ +//package org.openmrs.module.cashier.api.impl; +// +//import static org.mockito.Mockito.times; +//import static org.mockito.Mockito.verify; +//import static org.powermock.api.mockito.PowerMockito.mock; +//import static org.powermock.api.mockito.PowerMockito.mockStatic; +//import static org.powermock.api.mockito.PowerMockito.when; +// +//import org.junit.*; +//import org.junit.Before; +//import org.junit.BeforeClass; +//import org.junit.Rule; +//import org.junit.Test; +//import org.openmrs.api.APIException; +//import org.openmrs.api.context.Context; +//import org.openmrs.module.cashier.api.IBillService; +//import org.openmrs.module.cashier.api.IBillServiceTest; +//import org.openmrs.module.cashier.api.IReceiptNumberGenerator; +//import org.openmrs.module.cashier.api.ReceiptNumberGeneratorFactory; +//import org.openmrs.module.cashier.api.model.Bill; +//import org.powermock.core.classloader.annotations.PrepareForTest; +//import org.powermock.modules.agent.PowerMockAgent; +//import org.powermock.modules.junit4.rule.PowerMockRule; +// +//@PrepareForTest(ReceiptNumberGeneratorFactory.class) +//public class BillServiceImplTest extends IBillServiceTest { +// @Rule +// public PowerMockRule rule = new PowerMockRule(); +// +// @BeforeClass +// public static void beforeClass() throws Exception { +// PowerMockAgent.initializeIfNeeded(); +// } +// +// IReceiptNumberGenerator receiptNumberGenerator; +// +// @Before +// public void before() throws Exception { +// super.before(); +// +// mockStatic(ReceiptNumberGeneratorFactory.class); +// receiptNumberGenerator = mock(IReceiptNumberGenerator.class); +// +// when(ReceiptNumberGeneratorFactory.getGenerator()) +// .thenReturn(receiptNumberGenerator); +// } +// +// @Override +// protected IBillService createService() { +// return Context.getService(IBillService.class); +// } +// +// /** +// * @verifies Generate a new receipt number if one has not been defined. +// * @see BillServiceImpl#save(Bill) +// */ +// @Test +// public void save_shouldGenerateANewReceiptNumberIfOneHasNotBeenDefined() throws Exception { +// Bill bill = createEntity(true); +// bill.setReceiptNumber(null); +// +// String receiptNumber = "Test Number"; +// when(receiptNumberGenerator.generateNumber(bill)) +// .thenReturn(receiptNumber); +// +// service.save(bill); +// Context.flushSession(); +// +// Bill savedBill = service.getById(bill.getId()); +// Assert.assertEquals(receiptNumber, savedBill.getReceiptNumber()); +// +// verify(receiptNumberGenerator, times(1)).generateNumber(bill); +// } +// +// /** +// * @verifies Not generate a receipt number if one has already been defined. +// * @see BillServiceImpl#save(Bill) +// */ +// @Test +// public void save_shouldNotGenerateAReceiptNumberIfOneHasAlreadyBeenDefined() throws Exception { +// String receiptNumber = "Test Number"; +// Bill bill = createEntity(true); +// bill.setReceiptNumber(receiptNumber); +// +// service.save(bill); +// Context.flushSession(); +// +// Bill savedBill = service.getById(bill.getId()); +// Assert.assertEquals(receiptNumber, savedBill.getReceiptNumber()); +// +// verify(receiptNumberGenerator, times(0)).generateNumber(bill); +// } +// +// /** +// * @verifies Throw APIException if receipt number cannot be generated. +// * @see BillServiceImpl#save(Bill) +// */ +// @Test(expected = APIException.class) +// public void save_shouldThrowAPIExceptionIfReceiptNumberCannotBeGenerated() throws Exception { +// Bill bill = createEntity(true); +// bill.setReceiptNumber(null); +// +// when(receiptNumberGenerator.generateNumber(bill)) +// .thenThrow(new APIException("Test exception")); +// +// service.save(bill); +// } +//} diff --git a/api/src/test/java/org/openmrs/module/billing/impl/CashPointServiceImplTest.java b/api/src/test/java/org/openmrs/module/billing/impl/CashPointServiceImplTest.java deleted file mode 100644 index 061cbc0f..00000000 --- a/api/src/test/java/org/openmrs/module/billing/impl/CashPointServiceImplTest.java +++ /dev/null @@ -1,190 +0,0 @@ -/* - * The contents of this file are subject to the OpenMRS Public License - * Version 1.1 (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * http://license.openmrs.org - * - * Software distributed under the License is distributed on an "AS IS" - * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the - * License for the specific language governing rights and limitations - * under the License. - * - * Copyright (C) OpenMRS, LLC. All Rights Reserved. - */ - -package org.openmrs.module.billing.impl; - -import java.util.List; - -import org.apache.commons.lang3.RandomStringUtils; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.openmrs.Location; -import org.openmrs.api.LocationService; -import org.openmrs.api.context.Context; -import org.openmrs.module.billing.TestConstants; -import org.openmrs.module.billing.api.ICashPointService; -import org.openmrs.module.billing.api.model.CashPoint; -import org.openmrs.test.jupiter.BaseModuleContextSensitiveTest; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class CashPointServiceImplTest extends BaseModuleContextSensitiveTest { - - private ICashPointService cashPointService; - - private LocationService locationService; - - @BeforeEach - public void setup() { - cashPointService = Context.getService(ICashPointService.class); - locationService = Context.getLocationService(); - executeDataSet(TestConstants.CORE_DATASET2); - executeDataSet(TestConstants.BASE_DATASET_DIR + "CashPointTest.xml"); - } - - /** - * @see org.openmrs.module.billing.api.impl.CashPointServiceImpl#getCashPointsByLocation(Location, - * boolean) - */ - @Test - public void getCashPointsByLocation_shouldThrowIllegalArgumentExceptionIfLocationIsNull() { - assertThrows(IllegalArgumentException.class, () -> cashPointService.getCashPointsByLocation(null, false)); - } - - /** - * @see org.openmrs.module.billing.api.impl.CashPointServiceImpl#getCashPointsByLocation(Location, - * boolean) - */ - @Test - public void getCashPointsByLocation_shouldReturnCashPointsForLocation() { - Location location = locationService.getLocation(0); - assertNotNull(location); - List cashPoints = cashPointService.getCashPointsByLocation(location, false); - assertNotNull(cashPoints); - assertFalse(cashPoints.isEmpty()); - for (CashPoint cashPoint : cashPoints) { - assertEquals(location.getId(), cashPoint.getLocation().getId()); - } - } - - /** - * @see org.openmrs.module.billing.api.impl.CashPointServiceImpl#getCashPointsByLocation(Location, - * boolean) - */ - @Test - public void getCashPointsByLocation_shouldReturnEmptyListWhenLocationHasNoCashPoints() { - Location location = locationService.getLocation(999); - assertNotNull(location); - List cashPoints = cashPointService.getCashPointsByLocation(location, false); - assertNotNull(cashPoints); - assertTrue(cashPoints.isEmpty()); - } - - /** - * @see org.openmrs.module.billing.api.impl.CashPointServiceImpl#getCashPointsByLocationAndName(Location, - * String, boolean) - */ - @Test - public void getCashPointsByLocationAndName_shouldThrowIllegalArgumentExceptionIfLocationIsNull() { - assertThrows(IllegalArgumentException.class, - () -> cashPointService.getCashPointsByLocationAndName(null, "Test", false)); - } - - /** - * @see org.openmrs.module.billing.api.impl.CashPointServiceImpl#getCashPointsByLocationAndName(Location, - * String, boolean) - */ - @Test - public void getCashPointsByLocationAndName_shouldThrowIllegalArgumentExceptionIfNameIsNull() { - Location location = locationService.getLocation(0); - assertThrows(IllegalArgumentException.class, - () -> cashPointService.getCashPointsByLocationAndName(location, null, false)); - } - - /** - * @see org.openmrs.module.billing.api.impl.CashPointServiceImpl#getCashPointsByLocationAndName(Location, - * String, boolean) - */ - @Test - public void getCashPointsByLocationAndName_shouldThrowIllegalArgumentExceptionIfNameIsEmpty() { - Location location = locationService.getLocation(0); - assertThrows(IllegalArgumentException.class, - () -> cashPointService.getCashPointsByLocationAndName(location, "", false)); - } - - /** - * @see org.openmrs.module.billing.api.impl.CashPointServiceImpl#getCashPointsByLocationAndName(Location, - * String, boolean) - */ - @Test - public void getCashPointsByLocationAndName_shouldThrowIllegalArgumentExceptionIfNameIsTooLong() { - Location location = locationService.getLocation(0); - String longName = RandomStringUtils.randomAlphanumeric(256); - assertThrows(IllegalArgumentException.class, - () -> cashPointService.getCashPointsByLocationAndName(location, longName, false)); - } - - /** - * @see org.openmrs.module.billing.api.impl.CashPointServiceImpl#getCashPointsByLocationAndName(Location, - * String, boolean) - */ - @Test - public void getCashPointsByLocationAndName_shouldReturnCashPointsMatchingLocationAndName() { - Location location = locationService.getLocation(0); - List cashPoints = cashPointService.getCashPointsByLocationAndName(location, "Test", false); - assertNotNull(cashPoints); - assertFalse(cashPoints.isEmpty()); - for (CashPoint cashPoint : cashPoints) { - assertEquals(location.getId(), cashPoint.getLocation().getId()); - assertTrue(cashPoint.getName().startsWith("Test")); - } - } - - /** - * @see org.openmrs.module.billing.api.impl.CashPointServiceImpl#getCashPointsByLocationAndName(Location, - * String, boolean) - */ - @Test - public void getCashPointsByLocationAndName_shouldReturnEmptyListWhenNoMatch() { - Location location = locationService.getLocation(0); - List cashPoints = cashPointService.getCashPointsByLocationAndName(location, "Fake name", false); - assertNotNull(cashPoints); - assertTrue(cashPoints.isEmpty()); - } - - /** - * @see org.openmrs.module.billing.api.impl.CashPointServiceImpl#getById(int) - */ - @Test - public void getById_shouldReturnCashPointWithSpecifiedId() { - CashPoint cashPoint = cashPointService.getById(0); - assertNotNull(cashPoint); - assertEquals(0, cashPoint.getId()); - } - - /** - * @see org.openmrs.module.billing.api.impl.CashPointServiceImpl#getByUuid(String) - */ - @Test - public void getByUuid_shouldReturnCashPointWithSpecifiedUuid() { - CashPoint cashPoint = cashPointService.getByUuid("4028814B39BB04B90139BB04B98B0000"); - assertNotNull(cashPoint); - assertEquals("4028814B39BB04B90139BB04B98B0000", cashPoint.getUuid()); - } - - /** - * @see org.openmrs.module.billing.api.impl.CashPointServiceImpl#getAll() - */ - @Test - public void getAll_shouldReturnAllCashPoints() { - List cashPoints = cashPointService.getAll(); - assertNotNull(cashPoints); - assertFalse(cashPoints.isEmpty()); - assertEquals(7, cashPoints.size()); - } -} diff --git a/api/src/test/java/org/openmrs/module/billing/impl/CashierOptionsServiceGpImplTest.java b/api/src/test/java/org/openmrs/module/billing/impl/CashierOptionsServiceGpImplTest.java index 4501ecd3..53517792 100644 --- a/api/src/test/java/org/openmrs/module/billing/impl/CashierOptionsServiceGpImplTest.java +++ b/api/src/test/java/org/openmrs/module/billing/impl/CashierOptionsServiceGpImplTest.java @@ -1,190 +1,198 @@ -/* - * The contents of this file are subject to the OpenMRS Public License - * Version 1.1 (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * http://license.openmrs.org - * - * Software distributed under the License is distributed on an "AS IS" - * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the - * License for the specific language governing rights and limitations - * under the License. - * - * Copyright (C) OpenMRS, LLC. All Rights Reserved. - */ - -package org.openmrs.module.billing.impl; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.openmrs.api.AdministrationService; -import org.openmrs.api.context.Context; -import org.openmrs.module.billing.ModuleSettings; -import org.openmrs.module.billing.api.impl.CashierOptionsServiceGpImpl; -import org.openmrs.module.billing.api.model.CashierOptions; -import org.openmrs.test.jupiter.BaseModuleContextSensitiveTest; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class CashierOptionsServiceGpImplTest extends BaseModuleContextSensitiveTest { - - private CashierOptionsServiceGpImpl service; - - private AdministrationService adminService; - - @BeforeEach - public void setup() { - service = new CashierOptionsServiceGpImpl(); - adminService = Context.getAdministrationService(); - } - - /** - * @see org.openmrs.module.billing.api.impl.CashierOptionsServiceGpImpl#getOptions() - */ - @Test - public void getOptions_shouldReturnCashierOptionsWithDefaults() { - CashierOptions options = service.getOptions(); - assertNotNull(options); - assertFalse(options.isTimesheetRequired()); - assertEquals(CashierOptions.RoundingMode.MID, options.getRoundingMode()); - assertEquals(0, options.getRoundToNearest()); - } - - /** - * @see org.openmrs.module.billing.api.impl.CashierOptionsServiceGpImpl#getOptions() - */ - @Test - public void getOptions_shouldLoadDefaultReceiptReportIdFromGlobalProperty() { - adminService.setGlobalProperty(ModuleSettings.RECEIPT_REPORT_ID_PROPERTY, "123"); - - CashierOptions options = service.getOptions(); - assertNotNull(options); - assertEquals(123, options.getDefaultReceiptReportId()); - } - - /** - * @see org.openmrs.module.billing.api.impl.CashierOptionsServiceGpImpl#getOptions() - */ - @Test - public void getOptions_shouldHandleInvalidReceiptReportId() { - adminService.setGlobalProperty(ModuleSettings.RECEIPT_REPORT_ID_PROPERTY, "invalid"); - - CashierOptions options = service.getOptions(); - assertNotNull(options); - assertEquals(0, options.getDefaultReceiptReportId()); - } - - /** - * @see org.openmrs.module.billing.api.impl.CashierOptionsServiceGpImpl#getOptions() - */ - @Test - public void getOptions_shouldLoadTimesheetRequiredFromGlobalProperty() { - adminService.setGlobalProperty(ModuleSettings.TIMESHEET_REQUIRED_PROPERTY, "true"); - - CashierOptions options = service.getOptions(); - assertNotNull(options); - assertTrue(options.isTimesheetRequired()); - } - - /** - * @see org.openmrs.module.billing.api.impl.CashierOptionsServiceGpImpl#getOptions() - */ - @Test - public void getOptions_shouldDefaultToFalseIfTimesheetRequiredIsNotSpecified() { - adminService.setGlobalProperty(ModuleSettings.TIMESHEET_REQUIRED_PROPERTY, ""); - - CashierOptions options = service.getOptions(); - assertNotNull(options); - assertFalse(options.isTimesheetRequired()); - } - - /** - * @see org.openmrs.module.billing.api.impl.CashierOptionsServiceGpImpl#getOptions() - */ - @Test - public void getOptions_shouldHandleInvalidTimesheetRequiredValue() { - adminService.setGlobalProperty(ModuleSettings.TIMESHEET_REQUIRED_PROPERTY, "invalid"); - - CashierOptions options = service.getOptions(); - assertNotNull(options); - assertFalse(options.isTimesheetRequired()); - } - - /** - * @see org.openmrs.module.billing.api.impl.CashierOptionsServiceGpImpl#getOptions() - */ - @Test - public void getOptions_shouldSetDefaultRoundingOptionsWhenRoundingItemUuidIsEmpty() { - CashierOptions options = service.getOptions(); - assertNotNull(options); - assertEquals(CashierOptions.RoundingMode.MID, options.getRoundingMode()); - assertEquals(0, options.getRoundToNearest()); - } - - /** - * @see org.openmrs.module.billing.api.impl.CashierOptionsServiceGpImpl#getOptions() - */ - @Test - public void getOptions_shouldNotThrowExceptionIfNumericOptionsAreNull() { - adminService.setGlobalProperty(ModuleSettings.RECEIPT_REPORT_ID_PROPERTY, ""); - adminService.setGlobalProperty(ModuleSettings.ROUND_TO_NEAREST_PROPERTY, ""); - - assertDoesNotThrow(() -> { - CashierOptions options = service.getOptions(); - assertNotNull(options); - }); - } - - /** - * @see org.openmrs.module.billing.api.impl.CashierOptionsServiceGpImpl#getOptions() - */ - @Test - public void getOptions_shouldHandleMultiplePropertiesSet() { - adminService.setGlobalProperty(ModuleSettings.RECEIPT_REPORT_ID_PROPERTY, "456"); - adminService.setGlobalProperty(ModuleSettings.TIMESHEET_REQUIRED_PROPERTY, "true"); - - CashierOptions options = service.getOptions(); - assertNotNull(options); - assertEquals(456, options.getDefaultReceiptReportId()); - assertTrue(options.isTimesheetRequired()); - } - - /** - * @see org.openmrs.module.billing.api.impl.CashierOptionsServiceGpImpl#getOptions() - */ - @Test - public void getOptions_shouldLoadCashierOptionsFromTheDatabase() { - adminService.setGlobalProperty(ModuleSettings.RECEIPT_REPORT_ID_PROPERTY, "1"); - adminService.setGlobalProperty(ModuleSettings.TIMESHEET_REQUIRED_PROPERTY, "true"); - - CashierOptions options = service.getOptions(); - - assertNotNull(options); - assertEquals(1, options.getDefaultReceiptReportId()); - assertEquals(CashierOptions.RoundingMode.MID, options.getRoundingMode()); - assertEquals(0, options.getRoundToNearest()); - assertTrue(options.isTimesheetRequired()); - } - - /** - * @see org.openmrs.module.billing.api.impl.CashierOptionsServiceGpImpl#getOptions() - */ - @Test - public void getOptions_shouldHandleNullGlobalProperties() { - adminService.setGlobalProperty(ModuleSettings.RECEIPT_REPORT_ID_PROPERTY, null); - adminService.setGlobalProperty(ModuleSettings.ROUNDING_MODE_PROPERTY, null); - adminService.setGlobalProperty(ModuleSettings.ROUND_TO_NEAREST_PROPERTY, null); - adminService.setGlobalProperty(ModuleSettings.TIMESHEET_REQUIRED_PROPERTY, null); - - CashierOptions options = service.getOptions(); - - assertNotNull(options); - assertEquals(0, options.getDefaultReceiptReportId()); - assertEquals(CashierOptions.RoundingMode.MID, options.getRoundingMode()); - assertEquals(0, options.getRoundToNearest()); - assertFalse(options.isTimesheetRequired()); - } -} +///* +// * The contents of this file are subject to the OpenMRS Public License +// * Version 1.1 (the "License"); you may not use this file except in +// * compliance with the License. You may obtain a copy of the License at +// * http://license.openmrs.org +// * +// * Software distributed under the License is distributed on an "AS IS" +// * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the +// * License for the specific language governing rights and limitations +// * under the License. +// * +// * Copyright (C) OpenMRS, LLC. All Rights Reserved. +// */ +//package org.openmrs.module.cashier.api.impl; +// +//import static org.junit.Assert.assertFalse; +//import static org.junit.Assert.assertNotNull; +//import static org.powermock.api.mockito.PowerMockito.mock; +//import static org.powermock.api.mockito.PowerMockito.when; +// +//import java.io.ByteArrayOutputStream; +// +//import org.apache.log4j.Appender; +//import org.apache.log4j.Layout; +//import org.apache.log4j.Logger; +//import org.apache.log4j.SimpleLayout; +//import org.apache.log4j.WriterAppender; +//import org.junit.Assert; +//import org.junit.Before; +//import org.junit.Test; +//import org.junit.runner.RunWith; +//import org.openmrs.api.AdministrationService; +//import org.openmrs.module.cashier.ModuleSettings; +//import org.openmrs.module.cashier.api.impl.CashierOptionsServiceGpImpl; +//import org.openmrs.module.cashier.api.model.CashierOptions; +//import org.openmrs.module.openhmis.inventory.api.IItemDataService; +//import org.openmrs.module.openhmis.inventory.api.model.Item; +//import org.powermock.modules.junit4.PowerMockRunner; +// +//@RunWith(PowerMockRunner.class) +//public class CashierOptionsServiceGpImplTest { +// private CashierOptionsServiceGpImpl optionsService = null; +// private AdministrationService adminService = null; +// private IItemDataService itemService = null; +// +// @Before +// public void before() { +// adminService = mock(AdministrationService.class); +// itemService = mock(IItemDataService.class); +// +// optionsService = new CashierOptionsServiceGpImpl(); +// } +// +// /** +// * @verifies load cashier options from the database +// * @see CashierOptionsServiceGpImpl#getOptions() +// */ +// @Test +// public void getOptions_shouldLoadCashierOptionsFromTheDatabase() throws Exception { +// when(adminService.getGlobalProperty(ModuleSettings.RECEIPT_REPORT_ID_PROPERTY)) +// .thenReturn("1"); +// when(adminService.getGlobalProperty(ModuleSettings.ROUNDING_MODE_PROPERTY)) +// .thenReturn(CashierOptions.RoundingMode.MID.toString()); +// when(adminService.getGlobalProperty(ModuleSettings.ROUND_TO_NEAREST_PROPERTY)) +// .thenReturn("5"); +// when(adminService.getGlobalProperty(ModuleSettings.ROUNDING_ITEM_ID)) +// .thenReturn("1"); +// when(adminService.getGlobalProperty(ModuleSettings.TIMESHEET_REQUIRED_PROPERTY)) +// .thenReturn("true"); +// +// Item item = new Item(); +// when(itemService.getById(1)) +// .thenReturn(item); +// +// CashierOptions options = optionsService.getOptions(); +// +// Assert.assertNotNull(options); +// Assert.assertEquals(1, options.getDefaultReceiptReportId()); +// Assert.assertEquals(CashierOptions.RoundingMode.MID, options.getRoundingMode()); +// Assert.assertEquals(5, (int)options.getRoundToNearest()); +// Assert.assertEquals(item.getUuid(), options.getRoundingItemUuid()); +// Assert.assertEquals(true, options.isTimesheetRequired()); +// } +// +// /** +// * @verifies not throw exception if numeric options are null +// * @see CashierOptionsServiceGpImpl#getOptions() +// */ +// @Test +// public void getOptions_shouldNotThrowExceptionIfNumericOptionsAreNull() throws Exception { +// when(adminService.getGlobalProperty(ModuleSettings.RECEIPT_REPORT_ID_PROPERTY)) +// .thenReturn(null); +// when(adminService.getGlobalProperty(ModuleSettings.ROUNDING_MODE_PROPERTY)) +// .thenReturn(null); +// when(adminService.getGlobalProperty(ModuleSettings.ROUND_TO_NEAREST_PROPERTY)) +// .thenReturn(null); +// when(adminService.getGlobalProperty(ModuleSettings.ROUNDING_ITEM_ID)) +// .thenReturn(null); +// when(adminService.getGlobalProperty(ModuleSettings.TIMESHEET_REQUIRED_PROPERTY)) +// .thenReturn(null); +// +// CashierOptions options = optionsService.getOptions(); +// +// Assert.assertNotNull(options); +// } +// +// /** +// * @verifies default to false if timesheet required is not specified +// * @see CashierOptionsServiceGpImpl#getOptions() +// */ +// @Test +// public void getOptions_shouldDefaultToFalseIfTimesheetRequiredIsNotSpecified() throws Exception { +// when(adminService.getGlobalProperty(ModuleSettings.RECEIPT_REPORT_ID_PROPERTY)) +// .thenReturn(null); +// when(adminService.getGlobalProperty(ModuleSettings.ROUNDING_MODE_PROPERTY)) +// .thenReturn(null); +// when(adminService.getGlobalProperty(ModuleSettings.ROUND_TO_NEAREST_PROPERTY)) +// .thenReturn(null); +// when(adminService.getGlobalProperty(ModuleSettings.ROUNDING_ITEM_ID)) +// .thenReturn(null); +// when(adminService.getGlobalProperty(ModuleSettings.TIMESHEET_REQUIRED_PROPERTY)) +// .thenReturn(null); +// +// CashierOptions options = optionsService.getOptions(); +// +// Assert.assertNotNull(options); +// Assert.assertEquals(false, options.isTimesheetRequired()); +// } +// +// /** +// * @verifies log Error if Exception due to non-parsable rounding item id +// * @see CashierOptionsServiceGpImpl#getOptions() +// */ +// @Test +// public void getOptions_shouldLogErrorIfRoundingItemIdCannotBeParsed() throws Exception { +// +// when(adminService.getGlobalProperty(ModuleSettings.RECEIPT_REPORT_ID_PROPERTY)) +// .thenReturn(null); +// when(adminService.getGlobalProperty(ModuleSettings.ROUNDING_MODE_PROPERTY)) +// .thenReturn(CashierOptions.RoundingMode.FLOOR.toString()); +// when(adminService.getGlobalProperty(ModuleSettings.ROUND_TO_NEAREST_PROPERTY)) +// .thenReturn("5"); +// when(adminService.getGlobalProperty(ModuleSettings.ROUNDING_ITEM_ID)) +// .thenReturn("HELP"); +// +// Logger logger = Logger.getLogger(CashierOptionsServiceGpImpl.class); +// +// ByteArrayOutputStream out = new ByteArrayOutputStream(); +// Layout layout = new SimpleLayout(); +// Appender appender = new WriterAppender(layout, out); +// logger.addAppender(appender); +// +// try { +// optionsService.getOptions(); +// String logMsg = out.toString(); +// assertNotNull(logMsg); +// assertFalse((logMsg.trim()).equals("")); +// } finally { +// logger.removeAppender(appender); +// } +// } +// +// /** +// * @verifies log error if rouding item id is set but item cannot be found (and hence is null) +// * @see CashierOptionsServiceGpImpl#getOptions() +// */ +// @Test +// public void getOptions_shouldLogErrorIfRoundingItemIsNullDespiteIdGiven() throws Exception { +// +// when(adminService.getGlobalProperty(ModuleSettings.RECEIPT_REPORT_ID_PROPERTY)) +// .thenReturn(null); +// when(adminService.getGlobalProperty(ModuleSettings.ROUNDING_MODE_PROPERTY)) +// .thenReturn(CashierOptions.RoundingMode.FLOOR.toString()); +// when(adminService.getGlobalProperty(ModuleSettings.ROUND_TO_NEAREST_PROPERTY)) +// .thenReturn("5"); +// when(adminService.getGlobalProperty(ModuleSettings.ROUNDING_ITEM_ID)) +// .thenReturn("273423"); +// when(adminService.getGlobalProperty(ModuleSettings.TIMESHEET_REQUIRED_PROPERTY)) +// .thenReturn(null); +// +// Logger logger = Logger.getLogger(CashierOptionsServiceGpImpl.class); +// +// ByteArrayOutputStream out = new ByteArrayOutputStream(); +// Layout layout = new SimpleLayout(); +// Appender appender = new WriterAppender(layout, out); +// logger.addAppender(appender); +// +// try { +// optionsService.getOptions(); +// String logMsg = out.toString(); +// assertNotNull(logMsg); +// assertFalse((logMsg.trim()).equals("")); +// } finally { +// logger.removeAppender(appender); +// } +// } +// +//} diff --git a/api/src/test/java/org/openmrs/module/billing/validator/BillValidatorTest.java b/api/src/test/java/org/openmrs/module/billing/validator/BillValidatorTest.java deleted file mode 100644 index bc445c10..00000000 --- a/api/src/test/java/org/openmrs/module/billing/validator/BillValidatorTest.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * The contents of this file are subject to the OpenMRS Public License - * Version 1.1 (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * http://license.openmrs.org - * - * Software distributed under the License is distributed on an "AS IS" - * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the - * License for the specific language governing rights and limitations - * under the License. - * - * Copyright (C) OpenMRS, LLC. All Rights Reserved. - */ - -package org.openmrs.module.billing.validator; - -import static org.junit.jupiter.api.Assertions.*; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.openmrs.api.context.Context; -import org.openmrs.module.billing.TestConstants; -import org.openmrs.module.billing.api.BillService; -import org.openmrs.module.billing.api.model.Bill; -import org.openmrs.module.billing.api.model.BillStatus; -import org.openmrs.test.jupiter.BaseModuleContextSensitiveTest; -import org.springframework.validation.BindException; -import org.springframework.validation.Errors; - -/** - * Integration tests for {@link BillValidator} - */ -public class BillValidatorTest extends BaseModuleContextSensitiveTest { - - private BillValidator billValidator; - - private BillService billService; - - @BeforeEach - public void setup() throws Exception { - billValidator = new BillValidator(); - billService = Context.getService(BillService.class); - - executeDataSet(TestConstants.CORE_DATASET2); - executeDataSet(TestConstants.BASE_DATASET_DIR + "StockOperationType.xml"); - executeDataSet(TestConstants.BASE_DATASET_DIR + "PaymentModeTest.xml"); - executeDataSet(TestConstants.BASE_DATASET_DIR + "CashPointTest.xml"); - executeDataSet(TestConstants.BASE_DATASET_DIR + "BillTest.xml"); - } - - @Test - public void validate_shouldNotRejectPendingBill() { - Bill pendingBill = billService.getBill(2); - assertNotNull(pendingBill); - assertEquals(BillStatus.PENDING, pendingBill.getStatus()); - - Errors errors = new BindException(pendingBill, "bill"); - billValidator.validate(pendingBill, errors); - - assertFalse(errors.hasErrors()); - } - - @Test - public void validate_shouldRejectPaidBill() { - Bill paidBill = billService.getBill(1); - assertNotNull(paidBill); - assertEquals(BillStatus.PAID, paidBill.getStatus()); - - Errors errors = new BindException(paidBill, "bill"); - billValidator.validate(paidBill, errors); - - assertTrue(errors.hasErrors()); - assertTrue(errors.getGlobalError().getDefaultMessage() - .contains("Bill can only be modified when the bill is in PENDING state")); - assertTrue(errors.getGlobalError().getDefaultMessage().contains("PAID")); - } -} diff --git a/api/src/test/resources/org/openmrs/module/billing/api/include/BillExemptionTest.xml b/api/src/test/resources/org/openmrs/module/billing/api/include/BillExemptionTest.xml deleted file mode 100644 index 5844d760..00000000 --- a/api/src/test/resources/org/openmrs/module/billing/api/include/BillExemptionTest.xml +++ /dev/null @@ -1,107 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/api/src/test/resources/org/openmrs/module/billing/api/include/BillTest.xml b/api/src/test/resources/org/openmrs/module/billing/api/include/BillTest.xml index b2fb47b7..a4d909aa 100644 --- a/api/src/test/resources/org/openmrs/module/billing/api/include/BillTest.xml +++ b/api/src/test/resources/org/openmrs/module/billing/api/include/BillTest.xml @@ -1,45 +1,19 @@ - - - - - - - - - - - - - - - - - - diff --git a/api/src/test/resources/org/openmrs/module/billing/api/include/CoreTest-2.0.xml b/api/src/test/resources/org/openmrs/module/billing/api/include/CoreTest-2.0.xml index 88961198..382abef9 100644 --- a/api/src/test/resources/org/openmrs/module/billing/api/include/CoreTest-2.0.xml +++ b/api/src/test/resources/org/openmrs/module/billing/api/include/CoreTest-2.0.xml @@ -27,9 +27,6 @@ uuid="ef93c695-ac43-450a-93f8-4b2b4d50a3c8"/> - - - - - - - - - - - - - - - diff --git a/fhir/pom.xml b/fhir/pom.xml index 57b9d0bb..b9c1c647 100644 --- a/fhir/pom.xml +++ b/fhir/pom.xml @@ -6,7 +6,7 @@ org.openmrs.module billing - 2.0.0-SNAPSHOT + 1.3.3-SNAPSHOT billing-fhir @@ -54,22 +54,6 @@ fhir2-api test-jar - - - org.openmrs.module - fhir2-api-2.5 - - - - org.openmrs.module - fhir2-api-2.6 - - - - org.openmrs.module - fhir2-api-2.7 - - org.openmrs.module stockmanagement-api diff --git a/fhir/src/test/java/org/openmrs/module/billing/impl/FhirInvoiceServiceImplTest.java b/fhir/src/test/java/org/openmrs/module/billing/impl/FhirInvoiceServiceImplTest.java index 1bababb3..e9d0f1db 100644 --- a/fhir/src/test/java/org/openmrs/module/billing/impl/FhirInvoiceServiceImplTest.java +++ b/fhir/src/test/java/org/openmrs/module/billing/impl/FhirInvoiceServiceImplTest.java @@ -15,6 +15,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) diff --git a/lombok.config b/lombok.config deleted file mode 100644 index ec478262..00000000 --- a/lombok.config +++ /dev/null @@ -1,3 +0,0 @@ -# Lombok configuration for openmrs-module-billing - -lombok.getter.noIsPrefix = true \ No newline at end of file diff --git a/omod/pom.xml b/omod/pom.xml index 34610b20..2ff62aea 100644 --- a/omod/pom.xml +++ b/omod/pom.xml @@ -4,7 +4,7 @@ org.openmrs.module billing - 2.0.0-SNAPSHOT + 1.3.3-SNAPSHOT billing-omod diff --git a/omod/src/main/java/org/openmrs/module/billing/web/base/resource/BaseRestDataResource.java b/omod/src/main/java/org/openmrs/module/billing/web/base/resource/BaseRestDataResource.java index 4590b1ba..c4abaf39 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/base/resource/BaseRestDataResource.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/base/resource/BaseRestDataResource.java @@ -59,7 +59,12 @@ public abstract class BaseRestDataResource extends DataDe * @param The {@link OpenmrsObject} stored in the collection. */ public static void syncCollection(Collection base, Collection sync) { - syncCollection(base, sync, (collection, entity) -> collection.add(entity), new Action2, E>() { + syncCollection(base, sync, new Action2, E>() { + @Override + public void apply(Collection collection, E entity) { + collection.add(entity); + } + }, new Action2, E>() { @Override public void apply(Collection collection, E entity) { collection.remove(entity); @@ -107,7 +112,7 @@ public static void syncCollection(Collection base, @Override public E save(E delegate) { - return getService().saveBill(delegate); + return getService().save(delegate); } @Override diff --git a/omod/src/main/java/org/openmrs/module/billing/web/base/resource/BaseRestMetadataResource.java b/omod/src/main/java/org/openmrs/module/billing/web/base/resource/BaseRestMetadataResource.java index 817ded0d..b81a8076 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/base/resource/BaseRestMetadataResource.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/base/resource/BaseRestMetadataResource.java @@ -66,7 +66,7 @@ public abstract class BaseRestMetadataResource extend @Override public E save(E entity) { try { - return getService().saveBill(entity); + return getService().save(entity); } catch (PrivilegeException p) { LOG.error("Exception occured when trying to save entity <" + entity.getName() + "> as privilege is missing", p); throw new PrivilegeException("Can't save entity with name <" + entity.getName() + "> as privilege is missing"); diff --git a/omod/src/main/java/org/openmrs/module/billing/web/base/resource/BaseRestObjectResource.java b/omod/src/main/java/org/openmrs/module/billing/web/base/resource/BaseRestObjectResource.java index 64be3845..58299c2b 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/base/resource/BaseRestObjectResource.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/base/resource/BaseRestObjectResource.java @@ -71,7 +71,7 @@ public E save(E delegate) { } IObjectDataService service = Context.getService(clazz); - service.saveBill(delegate); + service.save(delegate); return delegate; } diff --git a/omod/src/main/java/org/openmrs/module/billing/web/legacyweb/controller/AbstractSequentialReceiptNumberGenerator.java b/omod/src/main/java/org/openmrs/module/billing/web/legacyweb/controller/AbstractSequentialReceiptNumberGenerator.java index c0aaecef..66c5fa3d 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/legacyweb/controller/AbstractSequentialReceiptNumberGenerator.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/legacyweb/controller/AbstractSequentialReceiptNumberGenerator.java @@ -62,7 +62,7 @@ public String post(@ModelAttribute("generator") SequentialReceiptNumberGenerator } // Save the generator settings - getService().saveBill(generator); + getService().save(generator); // Set the system generator ReceiptNumberGeneratorFactory.setGenerator(new SequentialReceiptNumberGenerator()); diff --git a/omod/src/main/java/org/openmrs/module/billing/web/legacyweb/controller/BillAddEditController.java b/omod/src/main/java/org/openmrs/module/billing/web/legacyweb/controller/BillAddEditController.java index 564dda31..642023d7 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/legacyweb/controller/BillAddEditController.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/legacyweb/controller/BillAddEditController.java @@ -26,7 +26,7 @@ import org.openmrs.api.PatientService; import org.openmrs.api.context.Context; import org.openmrs.module.billing.ModuleSettings; -import org.openmrs.module.billing.api.BillService; +import org.openmrs.module.billing.api.IBillService; import org.openmrs.module.billing.api.ICashierOptionsService; import org.openmrs.module.billing.api.base.util.UrlUtil; import org.openmrs.module.billing.api.model.Bill; @@ -159,17 +159,18 @@ private void addBillAttributes(ModelMap model, Bill bill, Patient patient) { model.addAttribute("patient", patient); model.addAttribute("cashPoint", bill.getCashPoint()); model.addAttribute("adjustmentReason", bill.getAdjustmentReason()); - if (!bill.getReceiptPrinted() || Context.hasPrivilege(PrivilegeConstants.REPRINT_RECEIPT)) { + if (!bill.isReceiptPrinted() + || (bill.isReceiptPrinted() && Context.hasPrivilege(PrivilegeConstants.REPRINT_RECEIPT))) { model.addAttribute("showPrint", true); } } private Bill getBillFromService(String billUuid) { - BillService service = Context.getService(BillService.class); + IBillService service = Context.getService(IBillService.class); Bill bill; try { - bill = service.getBillByUuid(billUuid); + bill = service.getByUuid(billUuid); } catch (APIException e) { LOG.error("Error when trying to get bill with ID <" + billUuid + ">", e); throw new APIException("Error when trying to get bill with ID <" + billUuid + ">"); diff --git a/omod/src/main/java/org/openmrs/module/billing/web/legacyweb/controller/CashierController.java b/omod/src/main/java/org/openmrs/module/billing/web/legacyweb/controller/CashierController.java index 2621761a..f73b7d70 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/legacyweb/controller/CashierController.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/legacyweb/controller/CashierController.java @@ -127,7 +127,7 @@ public String post(Timesheet timesheet, Errors errors, WebRequest request, Model return null; } - Context.getService(ITimesheetService.class).saveBill(timesheet); + Context.getService(ITimesheetService.class).save(timesheet); if (StringUtils.isEmpty(returnUrl)) { returnUrl = "redirect:"; diff --git a/omod/src/main/java/org/openmrs/module/billing/web/legacyweb/controller/PatientBillHistoryController.java b/omod/src/main/java/org/openmrs/module/billing/web/legacyweb/controller/PatientBillHistoryController.java index 36f156d3..320308aa 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/legacyweb/controller/PatientBillHistoryController.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/legacyweb/controller/PatientBillHistoryController.java @@ -17,7 +17,7 @@ import org.apache.log4j.Logger; import org.openmrs.api.context.Context; -import org.openmrs.module.billing.api.BillService; +import org.openmrs.module.billing.api.IBillService; import org.openmrs.module.billing.api.model.Bill; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; @@ -33,10 +33,14 @@ public class PatientBillHistoryController { private static final Logger LOG = Logger.getLogger(PatientBillHistoryController.class); + public PatientBillHistoryController() { + + } + @RequestMapping(method = RequestMethod.GET) - public void billHistory(ModelMap model, @RequestParam(value = "patientUuid") String patientUuid) { + public void billHistory(ModelMap model, @RequestParam(value = "patientId", required = true) int patientId) { LOG.warn("In bill history controller"); - List bills = Context.getService(BillService.class).getBillsByPatientUuid(patientUuid, null); + List bills = Context.getService(IBillService.class).getBillsByPatientId(patientId, null); model.addAttribute("bills", bills); } } diff --git a/omod/src/main/java/org/openmrs/module/billing/web/legacyweb/filter/CashierLogoutFilter.java b/omod/src/main/java/org/openmrs/module/billing/web/legacyweb/filter/CashierLogoutFilter.java index 56ed08cb..ce3bef7f 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/legacyweb/filter/CashierLogoutFilter.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/legacyweb/filter/CashierLogoutFilter.java @@ -81,7 +81,7 @@ private void clockOutCashier() { if (cashierIsClockedIn(timesheet)) { timesheet.setClockOut(new Date()); - timesheetService.saveBill(timesheet); + timesheetService.save(timesheet); } } diff --git a/omod/src/main/java/org/openmrs/module/billing/web/rest/controller/CashierRestController.java b/omod/src/main/java/org/openmrs/module/billing/web/rest/controller/CashierRestController.java index 3d720d1e..d9c2f36b 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/rest/controller/CashierRestController.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/rest/controller/CashierRestController.java @@ -35,7 +35,7 @@ public Object get(@RequestBody BillableServiceMapper request) { BillableService billableService = request.billableServiceMapper(request); IBillableItemsService service = Context.getService(IBillableItemsService.class); - service.saveBill(billableService); + service.save(billableService); return true; } diff --git a/omod/src/main/java/org/openmrs/module/billing/web/rest/controller/ReceiptController.java b/omod/src/main/java/org/openmrs/module/billing/web/rest/controller/ReceiptController.java index d50fb53e..d61a4993 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/rest/controller/ReceiptController.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/rest/controller/ReceiptController.java @@ -16,7 +16,7 @@ import java.io.IOException; import org.openmrs.api.context.Context; -import org.openmrs.module.billing.api.BillService; +import org.openmrs.module.billing.api.IBillService; import org.openmrs.module.billing.api.model.Bill; import org.openmrs.module.webservices.rest.web.RestConstants; import org.openmrs.module.webservices.rest.web.v1_0.controller.BaseRestController; @@ -37,25 +37,22 @@ public class ReceiptController extends BaseRestController { @RequestMapping(method = RequestMethod.GET) - public ResponseEntity get(@RequestParam(value = "billUuid", required = false) String billUuid) - throws IOException { - BillService service = Context.getService(BillService.class); - Bill bill = service.getBillByUuid(billUuid); + public ResponseEntity get(@RequestParam(value = "billId", required = false) Integer billId) throws IOException { + IBillService service = Context.getService(IBillService.class); + Bill bill = service.getById(billId); if (bill == null) { - return new ResponseEntity<>(HttpStatus.NOT_FOUND); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); } byte[] pdfFile = service.downloadBillReceipt(bill); - if (pdfFile != null && pdfFile.length > 0) { + if (pdfFile.length > 0) { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_PDF); - headers.setContentLength(pdfFile.length); - headers.add(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"receipt-" + bill.getId() + ".pdf\""); return new ResponseEntity<>(pdfFile, headers, HttpStatus.OK); } else { - return new ResponseEntity<>(HttpStatus.NO_CONTENT); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); } } diff --git a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillExemptionResource.java b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillExemptionResource.java deleted file mode 100644 index 62c94116..00000000 --- a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillExemptionResource.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * The contents of this file are subject to the OpenMRS Public License - * Version 1.1 (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * http://license.openmrs.org - * - * Software distributed under the License is distributed on an "AS IS" - * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the - * License for the specific language governing rights and limitations - * under the License. - * - * Copyright (C) OpenMRS, LLC. All Rights Reserved. - */ -package org.openmrs.module.billing.web.rest.resource; - -import org.openmrs.api.context.Context; -import org.openmrs.module.billing.api.BillExemptionService; -import org.openmrs.module.billing.api.model.BillExemption; -import org.openmrs.module.billing.api.model.BillExemptionRule; -import org.openmrs.module.billing.api.model.ExemptionType; -import org.openmrs.module.billing.web.rest.controller.base.CashierResourceController; -import org.openmrs.module.webservices.rest.web.RequestContext; -import org.openmrs.module.webservices.rest.web.RestConstants; -import org.openmrs.module.webservices.rest.web.annotation.PropertySetter; -import org.openmrs.module.webservices.rest.web.annotation.Resource; -import org.openmrs.module.webservices.rest.web.representation.DefaultRepresentation; -import org.openmrs.module.webservices.rest.web.representation.FullRepresentation; -import org.openmrs.module.webservices.rest.web.representation.RefRepresentation; -import org.openmrs.module.webservices.rest.web.representation.Representation; -import org.openmrs.module.webservices.rest.web.resource.impl.DelegatingResourceDescription; -import org.openmrs.module.webservices.rest.web.resource.impl.MetadataDelegatingCrudResource; -import org.openmrs.module.webservices.rest.web.response.ResponseException; - -import java.util.List; - -/** - * REST resource representing a {@link BillExemption}. - */ -@Resource(name = RestConstants.VERSION_1 + CashierResourceController.BILLING_NAMESPACE + "/billExemption", - supportedClass = BillExemption.class, supportedOpenmrsVersions = {"2.0 - 2.*"}) -public class BillExemptionResource extends MetadataDelegatingCrudResource { - - @Override - public BillExemption newDelegate() { - return new BillExemption(); - } - - @Override - public BillExemption save(BillExemption delegate) { - return getService().save(delegate); - } - - @Override - public BillExemption getByUniqueId(String uniqueId) { - return getService().getBillingExemptionByUuid(uniqueId); - } - - @Override - public void delete(BillExemption delegate, String reason, RequestContext context) throws ResponseException { - if (delegate.getRetired()) { - return; - } - delegate.setRetired(true); - delegate.setRetireReason(reason); - getService().save(delegate); - } - - @Override - public void purge(BillExemption delegate, RequestContext context) throws ResponseException { - throw new UnsupportedOperationException("Purge is not supported for BillingExemption"); - } - - @Override - public DelegatingResourceDescription getRepresentationDescription(Representation rep) { - DelegatingResourceDescription description = new DelegatingResourceDescription(); - - if (rep instanceof RefRepresentation) { - description.addProperty("uuid"); - description.addProperty("name"); - description.addProperty("description"); - description.addProperty("retired"); - } else if (rep instanceof DefaultRepresentation) { - description.addProperty("uuid"); - description.addProperty("name"); - description.addProperty("description"); - description.addProperty("retired"); - description.addProperty("retireReason"); - description.addProperty("concept", Representation.REF); - description.addProperty("exemptionType"); - description.addProperty("rules", Representation.DEFAULT); - } else if (rep instanceof FullRepresentation) { - description.addProperty("uuid"); - description.addProperty("name"); - description.addProperty("description"); - description.addProperty("retired"); - description.addProperty("retireReason"); - description.addProperty("concept", Representation.DEFAULT); - description.addProperty("exemptionType"); - description.addProperty("rules", Representation.FULL); - description.addProperty("auditInfo"); - } - - return description; - } - - @Override - public DelegatingResourceDescription getCreatableProperties() { - DelegatingResourceDescription description = new DelegatingResourceDescription(); - description.addProperty("name"); - description.addProperty("description"); - description.addProperty("concept"); - description.addProperty("exemptionType"); - description.addProperty("rules"); - return description; - } - - @Override - public DelegatingResourceDescription getUpdatableProperties() { - return getCreatableProperties(); - } - - @PropertySetter("rules") - public void setRules(BillExemption instance, List rules) { - if (rules != null) { - for (BillExemptionRule rule : rules) { - rule.setBillingExemption(instance); - } - instance.setRules(rules); - } - } - - @PropertySetter("exemptionType") - public void setExemptionType(BillExemption instance, String exemptionType) { - if (exemptionType != null) { - instance.setExemptionType(ExemptionType.valueOf(exemptionType)); - } - } - - private BillExemptionService getService() { - return Context.getService(BillExemptionService.class); - } -} \ No newline at end of file diff --git a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillExemptionRuleResource.java b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillExemptionRuleResource.java deleted file mode 100644 index cf7c3ccc..00000000 --- a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillExemptionRuleResource.java +++ /dev/null @@ -1,161 +0,0 @@ -/* - * The contents of this file are subject to the OpenMRS Public License - * Version 1.1 (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * http://license.openmrs.org - * - * Software distributed under the License is distributed on an "AS IS" - * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the - * License for the specific language governing rights and limitations - * under the License. - * - * Copyright (C) OpenMRS, LLC. All Rights Reserved. - */ -package org.openmrs.module.billing.web.rest.resource; - -import org.apache.commons.lang.StringEscapeUtils; -import org.openmrs.api.context.Context; -import org.openmrs.module.billing.api.BillExemptionService; -import org.openmrs.module.billing.api.evaluator.ScriptType; -import org.openmrs.module.billing.api.model.BillExemption; -import org.openmrs.module.billing.api.model.BillExemptionRule; -import org.openmrs.module.webservices.rest.web.RequestContext; -import org.openmrs.module.webservices.rest.web.annotation.PropertySetter; -import org.openmrs.module.webservices.rest.web.annotation.SubResource; -import org.openmrs.module.webservices.rest.web.representation.DefaultRepresentation; -import org.openmrs.module.webservices.rest.web.representation.FullRepresentation; -import org.openmrs.module.webservices.rest.web.representation.RefRepresentation; -import org.openmrs.module.webservices.rest.web.representation.Representation; -import org.openmrs.module.webservices.rest.web.resource.api.PageableResult; -import org.openmrs.module.webservices.rest.web.resource.impl.DelegatingResourceDescription; -import org.openmrs.module.webservices.rest.web.resource.impl.DelegatingSubResource; -import org.openmrs.module.webservices.rest.web.resource.impl.NeedsPaging; -import org.openmrs.module.webservices.rest.web.response.ResourceDoesNotSupportOperationException; -import org.openmrs.module.webservices.rest.web.response.ResponseException; - -import java.util.ArrayList; -import java.util.List; - -/** - * REST sub-resource representing a {@link BillExemptionRule}. - */ -@SubResource(parent = BillExemptionResource.class, path = "rule", supportedClass = BillExemptionRule.class, - supportedOpenmrsVersions = {"2.0 - 2.*"}) -public class BillExemptionRuleResource extends DelegatingSubResource { - - @Override - public BillExemptionRule newDelegate() { - return new BillExemptionRule(); - } - - @Override - public BillExemptionRule save(BillExemptionRule delegate) { - BillExemption exemption = delegate.getBillingExemption(); - if (exemption != null) { - getService().save(exemption); - } - return delegate; - } - - @Override - public BillExemption getParent(BillExemptionRule instance) { - return instance.getBillingExemption(); - } - - @Override - public void setParent(BillExemptionRule instance, BillExemption parent) { - instance.setBillingExemption(parent); - } - - @Override - public PageableResult doGetAll(BillExemption parent, RequestContext context) throws ResponseException { - List rules = parent.getRules(); - if (rules == null) { - rules = new ArrayList<>(); - } - return new NeedsPaging<>(rules, context); - } - - @Override - public BillExemptionRule getByUniqueId(String uniqueId) { - throw new ResourceDoesNotSupportOperationException("BillingExemptionRule does not support lookup by UUID"); - } - - @Override - protected void delete(BillExemptionRule delegate, String reason, RequestContext context) throws ResponseException { - if (delegate.getVoided()) { - return; - } - delegate.setVoided(true); - delegate.setVoidReason(reason); - BillExemption exemption = delegate.getBillingExemption(); - if (exemption != null) { - getService().save(exemption); - } - } - - @Override - public void purge(BillExemptionRule delegate, RequestContext context) throws ResponseException { - BillExemption exemption = delegate.getBillingExemption(); - if (exemption != null) { - exemption.getRules().remove(delegate); - getService().save(exemption); - } - } - - @Override - public DelegatingResourceDescription getRepresentationDescription(Representation rep) { - DelegatingResourceDescription description = new DelegatingResourceDescription(); - - if (rep instanceof RefRepresentation) { - description.addProperty("uuid"); - description.addProperty("scriptType"); - description.addProperty("script"); - } else if (rep instanceof DefaultRepresentation) { - description.addProperty("uuid"); - description.addProperty("scriptType"); - description.addProperty("script"); - description.addProperty("voided"); - } else if (rep instanceof FullRepresentation) { - description.addProperty("uuid"); - description.addProperty("scriptType"); - description.addProperty("script"); - description.addProperty("voided"); - description.addProperty("voidReason"); - description.addProperty("auditInfo"); - } - - return description; - } - - @Override - public DelegatingResourceDescription getCreatableProperties() { - DelegatingResourceDescription description = new DelegatingResourceDescription(); - description.addProperty("scriptType"); - description.addProperty("script"); - return description; - } - - @Override - public DelegatingResourceDescription getUpdatableProperties() { - return getCreatableProperties(); - } - - @PropertySetter("scriptType") - public void setScriptType(BillExemptionRule instance, String scriptType) { - if (scriptType != null) { - instance.setScriptType(ScriptType.valueOf(scriptType)); - } - } - - @PropertySetter("script") - public void setScript(BillExemptionRule instance, String script) { - if (script != null) { - instance.setScript(StringEscapeUtils.unescapeHtml(script)); - } - } - - private BillExemptionService getService() { - return Context.getService(BillExemptionService.class); - } -} \ No newline at end of file diff --git a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillLineItemResource.java b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillLineItemResource.java index 42f4e73d..4d357c63 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillLineItemResource.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillLineItemResource.java @@ -59,9 +59,8 @@ public DelegatingResourceDescription getRepresentationDescription(Representation description.addProperty("priceUuid"); description.addProperty("lineItemOrder"); description.addProperty("paymentStatus"); - return description; } - return null; + return description; } @PropertySetter(value = "item") diff --git a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillResource.java b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillResource.java index b25990b7..c0b89f39 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillResource.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillResource.java @@ -14,23 +14,23 @@ package org.openmrs.module.billing.web.rest.resource; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.stream.Collectors; -import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.util.Strings; +import org.openmrs.Patient; import org.openmrs.Provider; import org.openmrs.User; import org.openmrs.api.AdministrationService; import org.openmrs.api.ProviderService; import org.openmrs.api.context.Context; import org.openmrs.module.billing.ModuleSettings; -import org.openmrs.module.billing.api.BillService; +import org.openmrs.module.billing.api.IBillService; +import org.openmrs.module.billing.api.ICashPointService; import org.openmrs.module.billing.api.ITimesheetService; -import org.openmrs.module.billing.api.base.PagingInfo; +import org.openmrs.module.billing.api.base.entity.IEntityDataService; import org.openmrs.module.billing.api.model.Bill; import org.openmrs.module.billing.api.model.BillLineItem; import org.openmrs.module.billing.api.model.BillStatus; @@ -40,21 +40,16 @@ import org.openmrs.module.billing.api.search.BillSearch; import org.openmrs.module.billing.api.util.RoundingUtil; import org.openmrs.module.billing.web.base.resource.BaseRestDataResource; -import org.openmrs.module.billing.web.base.resource.PagingUtil; import org.openmrs.module.billing.web.rest.controller.base.CashierResourceController; import org.openmrs.module.webservices.rest.web.RequestContext; import org.openmrs.module.webservices.rest.web.RestConstants; import org.openmrs.module.webservices.rest.web.annotation.PropertySetter; import org.openmrs.module.webservices.rest.web.annotation.Resource; import org.openmrs.module.webservices.rest.web.representation.DefaultRepresentation; -import org.openmrs.module.webservices.rest.web.representation.FullRepresentation; +import org.openmrs.module.webservices.rest.web.representation.RefRepresentation; import org.openmrs.module.webservices.rest.web.representation.Representation; -import org.openmrs.module.webservices.rest.web.resource.api.PageableResult; import org.openmrs.module.webservices.rest.web.resource.impl.AlreadyPaged; -import org.openmrs.module.webservices.rest.web.resource.impl.DataDelegatingCrudResource; import org.openmrs.module.webservices.rest.web.resource.impl.DelegatingResourceDescription; -import org.openmrs.module.webservices.rest.web.resource.impl.NeedsPaging; -import org.openmrs.module.webservices.rest.web.response.ResponseException; import org.springframework.web.client.RestClientException; /** @@ -62,11 +57,11 @@ */ @Resource(name = RestConstants.VERSION_1 + CashierResourceController.BILLING_NAMESPACE + "/bill", supportedClass = Bill.class, supportedOpenmrsVersions = {"2.0 - 2.*"}) -public class BillResource extends DataDelegatingCrudResource { +public class BillResource extends BaseRestDataResource { @Override public DelegatingResourceDescription getRepresentationDescription(Representation rep) { - if (rep instanceof DefaultRepresentation || rep instanceof FullRepresentation) { - DelegatingResourceDescription description = new DelegatingResourceDescription(); + DelegatingResourceDescription description = super.getRepresentationDescription(rep); + if (!(rep instanceof RefRepresentation)) { description.addProperty("adjustedBy", Representation.REF); description.addProperty("billAdjusted", Representation.REF); description.addProperty("cashPoint", Representation.REF); @@ -79,9 +74,8 @@ public DelegatingResourceDescription getRepresentationDescription(Representation description.addProperty("status"); description.addProperty("adjustmentReason"); description.addProperty("id"); - return description; } - return null; + return description; } @Override @@ -92,7 +86,7 @@ public DelegatingResourceDescription getCreatableProperties() { @PropertySetter("lineItems") public void setBillLineItems(Bill instance, List lineItems) { if (instance.getLineItems() == null) { - instance.setLineItems(new ArrayList<>(lineItems.size())); + instance.setLineItems(new ArrayList(lineItems.size())); } BaseRestDataResource.syncCollection(instance.getLineItems(), lineItems); for (BillLineItem item : instance.getLineItems()) { @@ -142,7 +136,7 @@ public Bill save(Bill bill) { if (bill.getId() == null) { if (bill.getCashier() == null) { - Provider cashier = getCurrentCashier(); + Provider cashier = getCurrentCashier(bill); if (cashier == null) { throw new RestClientException("Couldn't find Provider for the current user (" + Context.getAuthenticatedUser().getUsername() + ")"); @@ -155,7 +149,7 @@ public Bill save(Bill bill) { loadBillCashPoint(bill); } - // Now that all attributes have been set (i.e., payments and bill status) we can check to see if the bill + // Now that all all attributes have been set (i.e., payments and bill status) we can check to see if the bill // is fully paid. bill.synchronizeBillStatus(); if (bill.getStatus() == null) { @@ -163,51 +157,45 @@ public Bill save(Bill bill) { } } - return Context.getService(BillService.class).saveBill(bill); + return super.save(bill); } @Override protected AlreadyPaged doSearch(RequestContext context) { - BillSearch billSearch = buildBillSearchFromRequest(context); - PagingInfo pagingInfo = PagingUtil.getPagingInfoFromContext(context); - - BillService service = Context.getService(BillService.class); - List result = service.getBills(billSearch, pagingInfo); - - return new AlreadyPaged<>(context, result, pagingInfo.hasMoreResults(), pagingInfo.getTotalRecordCount()); - } + String patientUuid = context.getRequest().getParameter("patientUuid"); + String status = context.getRequest().getParameter("status"); + String cashPointUuid = context.getRequest().getParameter("cashPointUuid"); + Patient patient = Strings.isNotEmpty(patientUuid) ? Context.getPatientService().getPatientByUuid(patientUuid) : null; + BillStatus billStatus = Strings.isNotEmpty(status) ? BillStatus.valueOf(status.toUpperCase()) : null; + CashPoint cashPoint = Strings.isNotEmpty(cashPointUuid) ? Context.getService(ICashPointService.class).getByUuid(cashPointUuid) : null; - /** - * Gets a bill by UUID - * - * @param uniqueId The bill UUID. - * @return The bill with the specified UUID without voided line items. - */ - @Override - public Bill getByUniqueId(String uniqueId) { - if (StringUtils.isBlank(uniqueId)) { - return null; - } + Bill searchTemplate = new Bill(); + searchTemplate.setPatient(patient); + searchTemplate.setStatus(billStatus); + searchTemplate.setCashPoint(cashPoint); + IBillService service = Context.getService(IBillService.class); - return Context.getService(BillService.class).getBillByUuid(uniqueId); + List result = service.getBills(new BillSearch(searchTemplate, false)); + return new AlreadyPaged<>(context, result, false); } + @SuppressWarnings("unchecked") @Override - protected void delete(Bill bill, String s, RequestContext requestContext) throws ResponseException { - Context.getService(BillService.class).voidBill(bill, s); + public Class> getServiceClass() { + return (Class>) (Object) IBillService.class; } - @Override - public void purge(Bill bill, RequestContext requestContext) throws ResponseException { - Context.getService(BillService.class).purgeBill(bill); + public String getDisplayString(Bill instance) { + return instance.getReceiptNumber(); } + @Override public Bill newDelegate() { return new Bill(); } - private Provider getCurrentCashier() { + private Provider getCurrentCashier(Bill bill) { User currentUser = Context.getAuthenticatedUser(); ProviderService service = Context.getProviderService(); Collection providers = service.getProvidersByPerson(currentUser.getPerson()); @@ -246,41 +234,4 @@ private void loadBillCashPoint(Bill bill) { bill.setCashPoint(cashPoint); } } - - - private BillSearch buildBillSearchFromRequest(RequestContext context) { - BillSearch billSearch = new BillSearch(); - - String patientUuid = context.getRequest().getParameter("patientUuid"); - if (StringUtils.isNotBlank(patientUuid)) { - billSearch.setPatientUuid(patientUuid); - } - - String patientName = context.getRequest().getParameter("patientName"); - if (StringUtils.isNotBlank(patientName)) { - billSearch.setPatientName(patientName); - } - - String status = context.getRequest().getParameter("status"); - if (StringUtils.isNotBlank(status)) { - List statuses = Arrays.stream(status.split(",")) - .map(String::trim) - .filter(StringUtils::isNotBlank) - .map(s -> BillStatus.valueOf(s.toUpperCase())) - .collect(Collectors.toList()); - billSearch.setStatuses(statuses); - } - - String cashPointUuid = context.getRequest().getParameter("cashPointUuid"); - if (StringUtils.isNotBlank(cashPointUuid)) { - billSearch.setCashPointUuid(cashPointUuid); - } - - String includeAll = context.getRequest().getParameter("includeAll"); - if (StringUtils.isNotBlank(includeAll)) { - billSearch.setIncludeVoidedLineItems(Boolean.parseBoolean(includeAll)); - } - - return billSearch; - } } diff --git a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillableServiceResource.java b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillableServiceResource.java index 4a77564d..7ae91672 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillableServiceResource.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillableServiceResource.java @@ -16,14 +16,11 @@ import org.apache.logging.log4j.util.Strings; import org.openmrs.Concept; import org.openmrs.api.context.Context; -import org.openmrs.module.billing.api.base.entity.IMetadataDataService; -import org.openmrs.module.billing.api.model.BillableService; -import org.openmrs.module.billing.api.model.BillableServiceStatus; -import org.openmrs.module.billing.api.model.CashierItemPrice; import org.openmrs.module.billing.web.base.resource.BaseRestDataResource; -import org.openmrs.module.billing.web.base.resource.BaseRestMetadataResource; import org.openmrs.module.billing.web.rest.controller.base.CashierResourceController; import org.openmrs.module.billing.api.IBillableItemsService; +import org.openmrs.module.billing.api.base.entity.IEntityDataService; +import org.openmrs.module.billing.api.model.*; import org.openmrs.module.billing.api.search.BillableServiceSearch; import org.openmrs.module.webservices.rest.web.RequestContext; import org.openmrs.module.webservices.rest.web.RestConstants; @@ -43,7 +40,7 @@ @Resource(name = RestConstants.VERSION_1 + CashierResourceController.BILLING_NAMESPACE + "/billableService", supportedClass = BillableService.class, supportedOpenmrsVersions = {"2.0 - 2.*"}) -public class BillableServiceResource extends BaseRestMetadataResource { +public class BillableServiceResource extends BaseRestDataResource { @Override public BillableService newDelegate() { @@ -51,7 +48,7 @@ public BillableService newDelegate() { } @Override - public Class> getServiceClass() { + public Class> getServiceClass() { return IBillableItemsService.class; } diff --git a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/CashierItemPriceResource.java b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/CashierItemPriceResource.java index a4e6e850..e4943b51 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/CashierItemPriceResource.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/CashierItemPriceResource.java @@ -14,10 +14,10 @@ package org.openmrs.module.billing.web.rest.resource; import org.openmrs.api.context.Context; -import org.openmrs.module.billing.api.base.entity.IMetadataDataService; -import org.openmrs.module.billing.web.base.resource.BaseRestMetadataResource; +import org.openmrs.module.billing.web.base.resource.BaseRestDataResource; import org.openmrs.module.billing.web.rest.controller.base.CashierResourceController; import org.openmrs.module.billing.api.ICashierItemPriceService; +import org.openmrs.module.billing.api.base.entity.IEntityDataService; import org.openmrs.module.billing.api.model.CashierItemPrice; import org.openmrs.module.stockmanagement.api.StockManagementService; import org.openmrs.module.stockmanagement.api.model.StockItem; @@ -36,14 +36,14 @@ @Resource(name = RestConstants.VERSION_1 + CashierResourceController.BILLING_NAMESPACE + "/cashierItemPrice", supportedClass = CashierItemPrice.class, supportedOpenmrsVersions = {"2.0 - 2.*"}) -public class CashierItemPriceResource extends BaseRestMetadataResource { +public class CashierItemPriceResource extends BaseRestDataResource { @Override public CashierItemPrice newDelegate() { return new CashierItemPrice(); } @Override - public Class> getServiceClass() { + public Class> getServiceClass() { return ICashierItemPriceService.class; } diff --git a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/PaymentResource.java b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/PaymentResource.java index 38568608..b29958fd 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/PaymentResource.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/PaymentResource.java @@ -14,8 +14,8 @@ package org.openmrs.module.billing.web.rest.resource; import org.openmrs.api.context.Context; -import org.openmrs.module.billing.api.BillService; import org.openmrs.module.billing.web.base.resource.BaseRestDataResource; +import org.openmrs.module.billing.api.IBillService; import org.openmrs.module.billing.api.IPaymentModeService; import org.openmrs.module.billing.api.model.Bill; import org.openmrs.module.billing.api.model.Payment; @@ -48,18 +48,18 @@ public class PaymentResource extends DelegatingSubResourceproperty = 'cashier.receipt.logoPath' - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Create tables for billing exemptions and exemption rules - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/pom.xml b/pom.xml index a442aba8..9f552ecc 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.openmrs.module billing - 2.0.0-SNAPSHOT + 1.3.3-SNAPSHOT pom OpenMRS Billing Module Module to provide basic billing functionality @@ -48,14 +48,14 @@ - 2.7.8-SNAPSHOT + 2.4.0 UTF-8 2.0.9 1.8 - 2.4.0 + 2.4.0 8.0.2 1.4.0 - 2.4.0 + 2.0.0 1.18.38 @@ -63,6 +63,7 @@ + org.openmrs.api openmrs-api @@ -113,13 +114,13 @@ org.openmrs.module webservices.rest-omod - 2.49.0 + 2.9 provided org.openmrs.module webservices.rest-omod-common - 2.49.0 + 2.9 provided @@ -147,27 +148,6 @@ provided - - org.openmrs.module - fhir2-api-2.5 - ${fhir2Version} - provided - - - - org.openmrs.module - fhir2-api-2.6 - ${fhir2Version} - provided - - - - org.openmrs.module - fhir2-api-2.7 - ${fhir2Version} - provided - - org.openmrs.module fhir2-api @@ -344,12 +324,6 @@ provided - - org.mockito - mockito-inline - 3.12.4 - test - From fb91a009de746f57c0ac21a2dd13fb692f34a5eb Mon Sep 17 00:00:00 2001 From: Mutagubya Jonathan Date: Mon, 15 Dec 2025 14:50:54 +0300 Subject: [PATCH 08/20] Add default value to disable auto drug order bill (#8) * BillableService and CashierItemPrice should be metadata (#49) * BillabeService and CashierItemPrice are metadata and should be treated as such * Fixes from code review * Remove unused imports across project * Enhance unit tests for service implementations (#52) * Revert "Allow bills to be viewed or download in the browser (#50)" This reverts commit d0e9d3b194a7ea96570c26b1eda2d28a00f2b45d. * (feat) O3-5197: Enable custom REST representation support for Bill Resource (#54) * (fix) O3-5122: Bill should not return voided line items (#46) * (fix): Bill should not return voided line items * Remove voided line items and payments from service level * filter voided line items in get bill by receipt number * Add filtering logic to the existing null remover function * Add transaction annotation to implementation * Review feedback * rename include voided items param to include all * Review feedback * Review feedback * O3-5187: Add server-side pagination (#53) * (feat) O3-5057: Add server-side pagination * Use alreadypaged with totalcount * Correct rebase * O3-5178: Exclude voided payments from bill status calculation (#48) * (feat): Allow filtering for multiple statuses (#58) * O3-5215: Enable Custom Representation for BillLineItemResource and PaymentResource (#62) * O3-5211: Update Billing Status When a Bill Line Item is Deleted (#63) * (fix) Generated bill should not get corrupted (#59) * fix generating Bill Reciept * fetch bill Reciept by uuid * remove redundant logs and raduce reciept height * Add patient name filtering support in Bill search (#68) * O3-5200: Bill should allow modifications only in pending state (#64) * O3-5156: Fix Biling itemList update functionality (#55) * Fix for concurrent issue * Addition of the test case * TestAddition plus usage of Set * O3-5156: Test Completion * O3-5156: Test Completion * Test changes * Test changes * Final changes * Review comment changes * Review comment changes * Code review --------- Co-authored-by: Ian * Migrate to Platform 2.7.x and support Java 21 (#69) * O3-5067: Replace JSON-based billing exemptions with database-backed service implementation (#57) * O3-5246: Fixing the payments issue Post addition of Pending state check (#72) * Migrate the BillService to OpenMRSService (#77) * (feat) Add default value to Bill auto creation on Drug Orders --------- Co-authored-by: Ian <52504170+ibacher@users.noreply.github.com> Co-authored-by: Wikum Weerakutti Co-authored-by: Ian Co-authored-by: Nethmi Rodrigo Co-authored-by: Raj Prakash Co-authored-by: Mutesasira Moses Co-authored-by: JG <85500670+jayg2002@users.noreply.github.com> --- omod/src/main/resources/config.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/omod/src/main/resources/config.xml b/omod/src/main/resources/config.xml index 238c39fb..747fa349 100644 --- a/omod/src/main/resources/config.xml +++ b/omod/src/main/resources/config.xml @@ -152,10 +152,10 @@ Path for the Bill Reciept Logo - ${project.parent.artifactId}.disableDrugOrderBillAutoCreation Disable automatic bill creation for drug orders + true From 11f7fe19ec9c922261eb18073a606b1707ad2700 Mon Sep 17 00:00:00 2001 From: Mutagubya Jonathan Date: Wed, 17 Dec 2025 13:21:28 +0300 Subject: [PATCH 09/20] Sync billing module with upstream fixes and features (excluding Java 21 / Platform 2.7 migration) (#10) * BillableService and CashierItemPrice should be metadata (#49) * BillabeService and CashierItemPrice are metadata and should be treated as such * Fixes from code review * Remove unused imports across project * Enhance unit tests for service implementations (#52) * Revert "Allow bills to be viewed or download in the browser (#50)" This reverts commit d0e9d3b194a7ea96570c26b1eda2d28a00f2b45d. * (feat) O3-5197: Enable custom REST representation support for Bill Resource (#54) * (fix) O3-5122: Bill should not return voided line items (#46) * (fix): Bill should not return voided line items * Remove voided line items and payments from service level * filter voided line items in get bill by receipt number * Add filtering logic to the existing null remover function * Add transaction annotation to implementation * Review feedback * rename include voided items param to include all * Review feedback * Review feedback * O3-5187: Add server-side pagination (#53) * (feat) O3-5057: Add server-side pagination * Use alreadypaged with totalcount * Correct rebase * O3-5178: Exclude voided payments from bill status calculation (#48) * (feat): Allow filtering for multiple statuses (#58) * O3-5215: Enable Custom Representation for BillLineItemResource and PaymentResource (#62) * O3-5211: Update Billing Status When a Bill Line Item is Deleted (#63) * (fix) Generated bill should not get corrupted (#59) * fix generating Bill Reciept * fetch bill Reciept by uuid * remove redundant logs and raduce reciept height * Add patient name filtering support in Bill search (#68) * O3-5200: Bill should allow modifications only in pending state (#64) * O3-5156: Fix Biling itemList update functionality (#55) * Fix for concurrent issue * Addition of the test case * TestAddition plus usage of Set * O3-5156: Test Completion * O3-5156: Test Completion * Test changes * Test changes * Final changes * Review comment changes * Review comment changes * Code review --------- Co-authored-by: Ian --------- Co-authored-by: Ian <52504170+ibacher@users.noreply.github.com> Co-authored-by: Wikum Weerakutti Co-authored-by: Ian Co-authored-by: Nethmi Rodrigo Co-authored-by: Raj Prakash Co-authored-by: Mutesasira Moses Co-authored-by: JG <85500670+jayg2002@users.noreply.github.com> --- .../advice/GenerateBillFromOrderAdvice.java | 1 - .../OrderCreationMethodBeforeAdvice.java | 1 - .../module/billing/api/IBillService.java | 11 + .../billing/api/IBillableItemsService.java | 4 +- .../billing/api/ICashierItemPriceService.java | 4 +- .../module/billing/api/ItemPriceService.java | 4 +- .../api/impl/BillLineItemServiceImpl.java | 61 ++- .../billing/api/impl/BillServiceImpl.java | 147 ++++-- .../api/impl/BillableItemsServiceImpl.java | 10 +- .../impl/ICashierItemPriceServiceImpl.java | 10 +- .../api/impl/ItemPriceServiceImpl.java | 10 +- .../module/billing/api/model/Bill.java | 38 +- .../billing/api/model/BillLineItem.java | 7 +- .../billing/api/model/BillableService.java | 4 +- .../billing/api/model/CashierItemPrice.java | 4 +- .../module/billing/api/search/BillSearch.java | 65 ++- .../api/search/BillableServiceSearch.java | 4 +- .../openmrs/module/billing/util/Utils.java | 4 - api/src/main/resources/Bill.hbm.xml | 20 +- .../module/billing/IBillServiceTest.java | 464 ----------------- .../openmrs/module/billing/TestConstants.java | 2 + .../module/billing/api/model/BillTest.java | 327 ++++++++++++ .../impl/BillLineItemServiceImplTest.java | 195 +++++++ .../billing/impl/BillServiceImplTest.java | 482 +++++++++++++----- .../impl/CashPointServiceImplTest.java | 190 +++++++ .../impl/CashierOptionsServiceGpImplTest.java | 388 +++++++------- .../module/billing/api/include/BillTest.xml | 57 ++- .../billing/api/include/CoreTest-2.0.xml | 3 + .../api/include/StockOperationType.xml | 41 ++ .../impl/FhirInvoiceServiceImplTest.java | 1 - .../rest/controller/ReceiptController.java | 13 +- .../rest/resource/BillLineItemResource.java | 3 +- .../web/rest/resource/BillResource.java | 78 ++- .../resource/BillableServiceResource.java | 11 +- .../resource/CashierItemPriceResource.java | 8 +- .../web/rest/resource/PaymentResource.java | 6 +- omod/src/main/resources/liquibase.xml | 26 + pom.xml | 4 +- 38 files changed, 1801 insertions(+), 907 deletions(-) delete mode 100644 api/src/test/java/org/openmrs/module/billing/IBillServiceTest.java create mode 100644 api/src/test/java/org/openmrs/module/billing/api/model/BillTest.java create mode 100644 api/src/test/java/org/openmrs/module/billing/impl/BillLineItemServiceImplTest.java create mode 100644 api/src/test/java/org/openmrs/module/billing/impl/CashPointServiceImplTest.java create mode 100644 api/src/test/resources/org/openmrs/module/billing/api/include/StockOperationType.xml diff --git a/api/src/main/java/org/openmrs/module/billing/advice/GenerateBillFromOrderAdvice.java b/api/src/main/java/org/openmrs/module/billing/advice/GenerateBillFromOrderAdvice.java index 597771cb..1b68f203 100644 --- a/api/src/main/java/org/openmrs/module/billing/advice/GenerateBillFromOrderAdvice.java +++ b/api/src/main/java/org/openmrs/module/billing/advice/GenerateBillFromOrderAdvice.java @@ -25,7 +25,6 @@ import org.openmrs.module.billing.api.model.CashierItemPrice; import org.openmrs.module.billing.api.search.BillableServiceSearch; import org.openmrs.module.billing.exemptions.BillingExemptions; -import org.openmrs.module.billing.util.Utils; import org.openmrs.module.stockmanagement.api.StockManagementService; import org.openmrs.module.stockmanagement.api.model.StockItem; import org.springframework.aop.AfterReturningAdvice; diff --git a/api/src/main/java/org/openmrs/module/billing/advice/OrderCreationMethodBeforeAdvice.java b/api/src/main/java/org/openmrs/module/billing/advice/OrderCreationMethodBeforeAdvice.java index 4030df12..426756cb 100644 --- a/api/src/main/java/org/openmrs/module/billing/advice/OrderCreationMethodBeforeAdvice.java +++ b/api/src/main/java/org/openmrs/module/billing/advice/OrderCreationMethodBeforeAdvice.java @@ -44,7 +44,6 @@ import org.openmrs.module.billing.api.model.CashPoint; import org.openmrs.module.billing.api.model.CashierItemPrice; import org.openmrs.module.billing.api.search.BillableServiceSearch; -import org.openmrs.module.billing.util.Utils; import org.openmrs.module.stockmanagement.api.StockManagementService; import org.openmrs.module.stockmanagement.api.model.StockItem; import org.springframework.aop.MethodBeforeAdvice; diff --git a/api/src/main/java/org/openmrs/module/billing/api/IBillService.java b/api/src/main/java/org/openmrs/module/billing/api/IBillService.java index cdd705aa..51988163 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/IBillService.java +++ b/api/src/main/java/org/openmrs/module/billing/api/IBillService.java @@ -107,6 +107,17 @@ public interface IBillService extends IEntityDataService { @Authorized(PrivilegeConstants.VIEW_BILLS) Bill getByUuid(String uuid); + /** + * Gets a bill by UUID, optionally including voided line items. + * + * @param uuid The bill UUID. + * @param includeVoidedLineItems {@code true} to include voided line items, {@code false} to exclude + * them. + * @return The bill with the specified UUID. + */ + @Authorized({ PrivilegeConstants.VIEW_BILLS }) + Bill getByUuid(String uuid, boolean includeVoidedLineItems); + /** * Gets bill receipt using the specified {@link Bill} settings. * diff --git a/api/src/main/java/org/openmrs/module/billing/api/IBillableItemsService.java b/api/src/main/java/org/openmrs/module/billing/api/IBillableItemsService.java index 920b6117..d827a1d9 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/IBillableItemsService.java +++ b/api/src/main/java/org/openmrs/module/billing/api/IBillableItemsService.java @@ -15,13 +15,13 @@ import java.util.List; -import org.openmrs.module.billing.api.base.entity.IEntityDataService; +import org.openmrs.module.billing.api.base.entity.IMetadataDataService; import org.openmrs.module.billing.api.model.BillableService; import org.openmrs.module.billing.api.search.BillableServiceSearch; import org.springframework.transaction.annotation.Transactional; @Transactional -public interface IBillableItemsService extends IEntityDataService { +public interface IBillableItemsService extends IMetadataDataService { List findServices(final BillableServiceSearch search); } diff --git a/api/src/main/java/org/openmrs/module/billing/api/ICashierItemPriceService.java b/api/src/main/java/org/openmrs/module/billing/api/ICashierItemPriceService.java index 385831f6..b530d6e3 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/ICashierItemPriceService.java +++ b/api/src/main/java/org/openmrs/module/billing/api/ICashierItemPriceService.java @@ -13,9 +13,9 @@ */ package org.openmrs.module.billing.api; -import org.openmrs.module.billing.api.base.entity.IEntityDataService; +import org.openmrs.module.billing.api.base.entity.IMetadataDataService; import org.openmrs.module.billing.api.model.CashierItemPrice; import org.springframework.transaction.annotation.Transactional; @Transactional -public interface ICashierItemPriceService extends IEntityDataService {} +public interface ICashierItemPriceService extends IMetadataDataService {} diff --git a/api/src/main/java/org/openmrs/module/billing/api/ItemPriceService.java b/api/src/main/java/org/openmrs/module/billing/api/ItemPriceService.java index e1208085..38d767a2 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/ItemPriceService.java +++ b/api/src/main/java/org/openmrs/module/billing/api/ItemPriceService.java @@ -15,14 +15,14 @@ import java.util.List; -import org.openmrs.module.billing.api.base.entity.IEntityDataService; +import org.openmrs.module.billing.api.base.entity.IMetadataDataService; import org.openmrs.module.billing.api.model.BillableService; import org.openmrs.module.billing.api.model.CashierItemPrice; import org.openmrs.module.stockmanagement.api.model.StockItem; import org.springframework.transaction.annotation.Transactional; @Transactional -public interface ItemPriceService extends IEntityDataService { +public interface ItemPriceService extends IMetadataDataService { CashierItemPrice save(CashierItemPrice price); diff --git a/api/src/main/java/org/openmrs/module/billing/api/impl/BillLineItemServiceImpl.java b/api/src/main/java/org/openmrs/module/billing/api/impl/BillLineItemServiceImpl.java index 75473c35..561360e2 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/impl/BillLineItemServiceImpl.java +++ b/api/src/main/java/org/openmrs/module/billing/api/impl/BillLineItemServiceImpl.java @@ -13,9 +13,12 @@ */ package org.openmrs.module.billing.api.impl; +import org.openmrs.api.context.Context; import org.openmrs.module.billing.api.BillLineItemService; +import org.openmrs.module.billing.api.IBillService; import org.openmrs.module.billing.api.base.entity.impl.BaseEntityDataServiceImpl; import org.openmrs.module.billing.api.base.entity.security.IEntityAuthorizationPrivileges; +import org.openmrs.module.billing.api.model.Bill; import org.openmrs.module.billing.api.model.BillLineItem; import org.springframework.transaction.annotation.Transactional; @@ -29,7 +32,14 @@ protected IEntityAuthorizationPrivileges getPrivileges() { @Override protected void validate(BillLineItem object) { - + if (object != null && object.getBill() != null) { + Bill bill = object.getBill(); + if (!bill.isPending()) { + throw new IllegalStateException( + "Line items can only be modified when the bill is in PENDING state. Current status: " + + bill.getStatus()); + } + } } @Override @@ -51,4 +61,53 @@ public String getPurgePrivilege() { public String getGetPrivilege() { return null; } + + @Override + public BillLineItem voidEntity(BillLineItem entity, String reason) { + BillLineItem voidedLineItem = super.voidEntity(entity, reason); + + if (voidedLineItem != null && voidedLineItem.getBill() != null) { + Bill bill = voidedLineItem.getBill(); + bill.synchronizeBillStatus(); + } + + return voidedLineItem; + } + + @Override + public BillLineItem unvoidEntity(BillLineItem entity) { + BillLineItem unvoidedLineItem = super.unvoidEntity(entity); + + if (unvoidedLineItem != null && unvoidedLineItem.getBill() != null) { + Bill bill = unvoidedLineItem.getBill(); + bill.synchronizeBillStatus(); + } + + return unvoidedLineItem; + } + + @Override + public void purge(BillLineItem entity) { + Bill bill = null; + if (entity != null && entity.getBill() != null) { + bill = entity.getBill(); + // Validate before purging (purge doesn't call validate()) + if (!bill.isPending()) { + throw new IllegalStateException( + "Line items can only be modified when the bill is in PENDING state. Current status: " + + bill.getStatus()); + } + } + + super.purge(entity); + + if (bill != null) { + // Remove the line item from the bill's collection + bill.removeLineItem(entity); + bill.synchronizeBillStatus(); + // Save the bill to persist the collection change + IBillService billService = Context.getService(IBillService.class); + billService.save(bill); + } + } } diff --git a/api/src/main/java/org/openmrs/module/billing/api/impl/BillServiceImpl.java b/api/src/main/java/org/openmrs/module/billing/api/impl/BillServiceImpl.java index a6f2c64d..375de166 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/impl/BillServiceImpl.java +++ b/api/src/main/java/org/openmrs/module/billing/api/impl/BillServiceImpl.java @@ -21,8 +21,11 @@ import java.net.URL; import java.security.AccessControlException; import java.text.DecimalFormat; +import java.util.ArrayList; import java.util.Date; +import java.util.HashSet; import java.util.List; +import java.util.Set; import com.itextpdf.io.font.constants.StandardFonts; import com.itextpdf.io.image.ImageDataFactory; @@ -59,7 +62,6 @@ import org.openmrs.module.billing.api.base.PagingInfo; import org.openmrs.module.billing.api.base.entity.impl.BaseEntityDataServiceImpl; import org.openmrs.module.billing.api.base.entity.security.IEntityAuthorizationPrivileges; -import org.openmrs.module.billing.api.base.f.Action1; import org.openmrs.module.billing.api.model.Bill; import org.openmrs.module.billing.api.model.BillLineItem; import org.openmrs.module.billing.api.model.BillStatus; @@ -137,14 +139,21 @@ public Bill save(Bill bill) { if (!bills.isEmpty()) { Bill billToUpdate = bills.get(0); billToUpdate.setStatus(BillStatus.PENDING); + + // Handle the case where bill and billToUpdate are the same object reference + // (Hibernate session cache returns same managed instance) + Set existingItemsSet = new HashSet<>(billToUpdate.getLineItems()); + for (BillLineItem item : bill.getLineItems()) { - item.setBill(billToUpdate); - billToUpdate.getLineItems().add(item); + // Only add if not already present (BillLineItem.equals() handles comparison) + if (!existingItemsSet.contains(item)) { + item.setBill(billToUpdate); + billToUpdate.getLineItems().add(item); + } } - // Calculate the total payments made on the bill - BigDecimal totalPaid = billToUpdate.getPayments().stream().map(Payment::getAmountTendered) - .reduce(BigDecimal.ZERO, BigDecimal::add); + // Calculate the total payments made on the bill (excluding voided payments) + BigDecimal totalPaid = billToUpdate.getTotalPayments(); // Check if the bill is fully paid if (totalPaid.compareTo(billToUpdate.getTotal()) >= 0) { @@ -152,7 +161,7 @@ public Bill save(Bill bill) { } else { billToUpdate.setStatus(BillStatus.PENDING); } - + // Save the updated bill return super.save(billToUpdate); } @@ -165,6 +174,18 @@ public Bill save(Bill bill) { @Authorized({ PrivilegeConstants.VIEW_BILLS }) @Transactional(readOnly = true) public Bill getBillByReceiptNumber(String receiptNumber) { + return getBillByReceiptNumber(receiptNumber, false); + } + + /** + * Gets a bill by receipt number, optionally including voided line items. + * + * @param receiptNumber The receipt number. + * @param includeVoidedLineItems {@code true} to include voided line items, {@code false} to exclude + * them. + * @return The bill with the specified receipt number. + */ + public Bill getBillByReceiptNumber(String receiptNumber, boolean includeVoidedLineItems) { if (StringUtils.isEmpty(receiptNumber)) { throw new IllegalArgumentException("The receipt number must be defined."); } @@ -176,7 +197,7 @@ public Bill getBillByReceiptNumber(String receiptNumber) { criteria.add(Restrictions.eq("receiptNumber", receiptNumber)); Bill bill = getRepository().selectSingle(getEntityClass(), criteria); - removeNullLineItems(bill); + removeNullLineItems(bill, includeVoidedLineItems); return bill; } @@ -191,6 +212,19 @@ public List getBillsByPatient(Patient patient, PagingInfo paging) { @Override public List getBillsByPatientId(int patientId, PagingInfo paging) { + return getBillsByPatientId(patientId, paging, false); + } + + /** + * Gets all bills for the specified patient, optionally including voided line items. + * + * @param patientId The patient ID. + * @param paging The paging information. + * @param includeVoidedLineItems {@code true} to include voided line items, {@code false} to exclude + * them. + * @return All bills for the specified patient. + */ + public List getBillsByPatientId(int patientId, PagingInfo paging, boolean includeVoidedLineItems) { if (patientId < 0) { throw new IllegalArgumentException("The patient id must be a valid identifier."); } @@ -200,7 +234,7 @@ public List getBillsByPatientId(int patientId, PagingInfo paging) { criteria.addOrder(Order.desc("id")); List results = getRepository().select(getEntityClass(), createPagingCriteria(paging, criteria)); - removeNullLineItems(results); + removeNullLineItems(results, includeVoidedLineItems); return results; } @@ -218,13 +252,14 @@ public List getBills(final BillSearch billSearch, PagingInfo pagingInfo) { throw new NullPointerException("The bill search template must be defined."); } - return executeCriteria(Bill.class, pagingInfo, new Action1() { - - @Override - public void apply(Criteria criteria) { - billSearch.updateCriteria(criteria); - } - }); + boolean includeVoidedLineItems = billSearch.getIncludeVoidedLineItems() != null + && billSearch.getIncludeVoidedLineItems(); + + List results = executeCriteria(Bill.class, pagingInfo, billSearch::updateCriteria); + + removeNullLineItems(results, includeVoidedLineItems); + + return results; } /* @@ -234,21 +269,51 @@ These methods are overridden to ensure that any null line items (created as part @Override public List getAll(boolean includeVoided, PagingInfo pagingInfo) { List results = super.getAll(includeVoided, pagingInfo); - removeNullLineItems(results); + removeNullLineItems(results, false); return results; } @Override public Bill getById(int entityId) { Bill bill = super.getById(entityId); - removeNullLineItems(bill); + removeNullLineItems(bill, false); + return bill; + } + + /** + * Gets a bill by ID, optionally including voided line items. + * + * @param entityId The bill ID. + * @param includeVoidedLineItems {@code true} to include voided line items, {@code false} to exclude + * them. + * @return The bill with the specified ID. + */ + public Bill getById(int entityId, boolean includeVoidedLineItems) { + Bill bill = super.getById(entityId); + removeNullLineItems(bill, includeVoidedLineItems); return bill; } @Override public Bill getByUuid(String uuid) { Bill bill = super.getByUuid(uuid); - removeNullLineItems(bill); + removeNullLineItems(bill, false); + return bill; + } + + /** + * Gets a bill by UUID, optionally including voided line items. + * + * @param uuid The bill UUID. + * @param includeVoidedLineItems {@code true} to include voided line items, {@code false} to exclude + * them. + * @return The bill with the specified UUID. + */ + @Transactional(readOnly = true) + @Override + public Bill getByUuid(String uuid, boolean includeVoidedLineItems) { + Bill bill = super.getByUuid(uuid); + removeNullLineItems(bill, includeVoidedLineItems); return bill; } @@ -275,7 +340,7 @@ public byte[] downloadBillReceipt(Bill bill) { * Thermal printer: 4 x 10 inches paper 4 inches = 4 x 72 = 288 5 inches = 10 x 72 = 720 */ int FONT_SIZE_12 = 12; - Rectangle thermalPrinterPageSize = new Rectangle(288, 14400); + Rectangle thermalPrinterPageSize = new Rectangle(288, 720); PdfFont timesRoman; PdfFont courierBold; @@ -443,8 +508,8 @@ public byte[] downloadBillReceipt(Bill bill) { setInnerCellBorder(amountDueSection, Border.NO_BORDER); setInnerCellBorder(totalsSection, Border.NO_BORDER); - try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); - PdfDocument pdfDoc = new PdfDocument(new PdfWriter(bos)); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try (PdfDocument pdfDoc = new PdfDocument(new PdfWriter(bos)); Document doc = new Document(pdfDoc, new PageSize(thermalPrinterPageSize))) { doc.setMargins(6, 12, 2, 12); if (logoSection != null) { @@ -463,14 +528,13 @@ public byte[] downloadBillReceipt(Bill bill) { doc.add(divider); doc.add(new Paragraph("You were served by " + bill.getCashier().getName()).setFont(footerSectionFont) .setFontSize(8).setTextAlignment(TextAlignment.CENTER)); - - return bos.toByteArray(); } - catch (IOException e) { + catch (Exception e) { LOG.error("Exception caught while writing PDF to stream", e); + return bos.toByteArray(); } - return new byte[0]; + return bos.toByteArray(); } private void setInnerCellBorder(Table table, Border border) { @@ -502,31 +566,30 @@ private void addFormattedCell(Table table, String cellValue, PdfFont font, TextA @Override public List getAll() { List results = super.getAll(); - removeNullLineItems(results); + removeNullLineItems(results, false); return results; } - private void removeNullLineItems(List bills) { - if (bills == null || bills.size() == 0) { + private void removeNullLineItems(List bills, boolean includeVoidedLineItems) { + if (bills == null || bills.isEmpty()) { return; } for (Bill bill : bills) { - removeNullLineItems(bill); + removeNullLineItems(bill, includeVoidedLineItems); } } - private void removeNullLineItems(Bill bill) { + private void removeNullLineItems(Bill bill, boolean includeVoidedLineItems) { if (bill == null) { return; } - // Search for any null line items (due to a bug in 1.7.0) and remove them from the line items - int index = bill.getLineItems().indexOf(null); - while (index >= 0) { - bill.getLineItems().remove(index); - - index = bill.getLineItems().indexOf(null); + // Search for any null line items (due to a bug in 1.7.0) and remove them from + // the line items + if (bill.getLineItems() != null) { + bill.getLineItems().removeIf( + lineItem -> lineItem == null || (!includeVoidedLineItems && Boolean.TRUE.equals(lineItem.getVoided()))); } } @@ -570,6 +633,14 @@ public List searchBill(Patient patient) { criteria.add(Restrictions.lt("dateCreated", endOfDayDate)); criteria.addOrder(Order.desc("id")); - return criteria.list(); + List results = criteria.list(); + List bills = new ArrayList<>(); + for (Object obj : results) { + if (obj instanceof Bill) { + bills.add((Bill) obj); + } + } + removeNullLineItems(bills, false); + return bills; } } diff --git a/api/src/main/java/org/openmrs/module/billing/api/impl/BillableItemsServiceImpl.java b/api/src/main/java/org/openmrs/module/billing/api/impl/BillableItemsServiceImpl.java index 449065ee..20a75948 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/impl/BillableItemsServiceImpl.java +++ b/api/src/main/java/org/openmrs/module/billing/api/impl/BillableItemsServiceImpl.java @@ -17,15 +17,15 @@ import org.hibernate.Criteria; import org.openmrs.module.billing.api.IBillableItemsService; -import org.openmrs.module.billing.api.base.entity.impl.BaseEntityDataServiceImpl; -import org.openmrs.module.billing.api.base.entity.security.IEntityAuthorizationPrivileges; +import org.openmrs.module.billing.api.base.entity.impl.BaseMetadataDataServiceImpl; +import org.openmrs.module.billing.api.base.entity.security.IMetadataAuthorizationPrivileges; import org.openmrs.module.billing.api.base.f.Action1; import org.openmrs.module.billing.api.model.BillableService; import org.openmrs.module.billing.api.search.BillableServiceSearch; import org.springframework.transaction.annotation.Transactional; @Transactional -public class BillableItemsServiceImpl extends BaseEntityDataServiceImpl implements IEntityAuthorizationPrivileges, IBillableItemsService { +public class BillableItemsServiceImpl extends BaseMetadataDataServiceImpl implements IMetadataAuthorizationPrivileges, IBillableItemsService { @Override public List findServices(final BillableServiceSearch serviceSearch) { @@ -39,7 +39,7 @@ public void apply(Criteria criteria) { } @Override - protected IEntityAuthorizationPrivileges getPrivileges() { + protected IMetadataAuthorizationPrivileges getPrivileges() { return this; } @@ -49,7 +49,7 @@ protected void validate(BillableService object) { } @Override - public String getVoidPrivilege() { + public String getRetirePrivilege() { return null; } diff --git a/api/src/main/java/org/openmrs/module/billing/api/impl/ICashierItemPriceServiceImpl.java b/api/src/main/java/org/openmrs/module/billing/api/impl/ICashierItemPriceServiceImpl.java index 454f8db3..4738a43f 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/impl/ICashierItemPriceServiceImpl.java +++ b/api/src/main/java/org/openmrs/module/billing/api/impl/ICashierItemPriceServiceImpl.java @@ -14,16 +14,16 @@ package org.openmrs.module.billing.api.impl; import org.openmrs.module.billing.api.ICashierItemPriceService; -import org.openmrs.module.billing.api.base.entity.impl.BaseEntityDataServiceImpl; -import org.openmrs.module.billing.api.base.entity.security.IEntityAuthorizationPrivileges; +import org.openmrs.module.billing.api.base.entity.impl.BaseMetadataDataServiceImpl; +import org.openmrs.module.billing.api.base.entity.security.IMetadataAuthorizationPrivileges; import org.openmrs.module.billing.api.model.CashierItemPrice; import org.springframework.transaction.annotation.Transactional; @Transactional -public class ICashierItemPriceServiceImpl extends BaseEntityDataServiceImpl implements IEntityAuthorizationPrivileges, ICashierItemPriceService { +public class ICashierItemPriceServiceImpl extends BaseMetadataDataServiceImpl implements IMetadataAuthorizationPrivileges, ICashierItemPriceService { @Override - protected IEntityAuthorizationPrivileges getPrivileges() { + protected IMetadataAuthorizationPrivileges getPrivileges() { return this; } @@ -33,7 +33,7 @@ protected void validate(CashierItemPrice object) { } @Override - public String getVoidPrivilege() { + public String getRetirePrivilege() { return null; } diff --git a/api/src/main/java/org/openmrs/module/billing/api/impl/ItemPriceServiceImpl.java b/api/src/main/java/org/openmrs/module/billing/api/impl/ItemPriceServiceImpl.java index 75243531..84a7b50d 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/impl/ItemPriceServiceImpl.java +++ b/api/src/main/java/org/openmrs/module/billing/api/impl/ItemPriceServiceImpl.java @@ -21,20 +21,20 @@ import org.hibernate.criterion.Order; import org.hibernate.criterion.Restrictions; import org.openmrs.module.billing.api.ItemPriceService; -import org.openmrs.module.billing.api.base.entity.impl.BaseEntityDataServiceImpl; -import org.openmrs.module.billing.api.base.entity.security.IEntityAuthorizationPrivileges; +import org.openmrs.module.billing.api.base.entity.impl.BaseMetadataDataServiceImpl; +import org.openmrs.module.billing.api.base.entity.security.IMetadataAuthorizationPrivileges; import org.openmrs.module.billing.api.model.BillableService; import org.openmrs.module.billing.api.model.CashierItemPrice; import org.openmrs.module.stockmanagement.api.model.StockItem; import org.springframework.transaction.annotation.Transactional; @Transactional -public class ItemPriceServiceImpl extends BaseEntityDataServiceImpl implements IEntityAuthorizationPrivileges, ItemPriceService { +public class ItemPriceServiceImpl extends BaseMetadataDataServiceImpl implements IMetadataAuthorizationPrivileges, ItemPriceService { private static final Log LOG = LogFactory.getLog(ItemPriceServiceImpl.class); @Override - protected IEntityAuthorizationPrivileges getPrivileges() { + protected IMetadataAuthorizationPrivileges getPrivileges() { return this; } @@ -50,7 +50,7 @@ public CashierItemPrice save(CashierItemPrice object) { } @Override - public String getVoidPrivilege() { + public String getRetirePrivilege() { return null; } diff --git a/api/src/main/java/org/openmrs/module/billing/api/model/Bill.java b/api/src/main/java/org/openmrs/module/billing/api/model/Bill.java index 205253df..7c502a3f 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/model/Bill.java +++ b/api/src/main/java/org/openmrs/module/billing/api/model/Bill.java @@ -83,6 +83,7 @@ public Boolean getReceiptPrinted() { public BigDecimal getTotal() { BigDecimal total = BigDecimal.ZERO; + List lineItems = getLineItems(); if (lineItems != null) { for (BillLineItem line : lineItems) { if (line != null && !line.getVoided()) { @@ -97,6 +98,7 @@ public BigDecimal getTotal() { public BigDecimal getTotalPayments() { BigDecimal total = BigDecimal.ZERO; + Set payments = getPayments(); if (payments != null) { for (Payment payment : payments) { if (payment != null && !payment.getVoided()) { @@ -108,13 +110,6 @@ public BigDecimal getTotalPayments() { return total; } - public BigDecimal getAmountPaid() { - BigDecimal total = getTotal(); - BigDecimal totalPayments = getTotalPayments(); - - return total.min(totalPayments); - } - @Override public Integer getId() { return billId; @@ -182,6 +177,14 @@ public List getLineItems() { } public void setLineItems(List lineItems) { + // Only validate if lineItems is already initialized + // This prevents validation during Hibernate entity loading (when lineItems is null) + // but still validates user modifications (when lineItems is already set) + if (this.lineItems != null && !isPending()) { + throw new IllegalStateException( + "Line items can only be modified when the bill is in PENDING state. Current status: " + + this.getStatus()); + } this.lineItems = lineItems; } @@ -220,6 +223,12 @@ public void addLineItem(BillLineItem item) { throw new NullPointerException("The list item to add must be defined."); } + if (!isPending()) { + throw new IllegalStateException( + "Line items can only be modified when the bill is in PENDING state. Current status: " + + this.getStatus()); + } + if (this.lineItems == null) { this.lineItems = new ArrayList(); } @@ -230,6 +239,11 @@ public void addLineItem(BillLineItem item) { public void removeLineItem(BillLineItem item) { if (item != null) { + if (!isPending()) { + throw new IllegalStateException( + "Line items can only be modified when the bill is in PENDING state. Current status: " + + this.getStatus()); + } if (this.lineItems != null) { this.lineItems.remove(item); } @@ -337,6 +351,16 @@ private void checkAuthorizedToAdjust() { } } + /** + * Checks if the bill is in PENDING state. + * + * @return {@code true} if the bill is new (no ID) or is in PENDING state, {@code false} otherwise + */ + public boolean isPending() { + // New bills (no ID) are considered pending, existing bills must be in PENDING state + return this.getId() == null || this.getStatus() == BillStatus.PENDING; + } + public void recalculateLineItemOrder() { int orderCounter = 0; for (BillLineItem lineItem : this.getLineItems()) { diff --git a/api/src/main/java/org/openmrs/module/billing/api/model/BillLineItem.java b/api/src/main/java/org/openmrs/module/billing/api/model/BillLineItem.java index 07c8d84b..60cedc09 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/model/BillLineItem.java +++ b/api/src/main/java/org/openmrs/module/billing/api/model/BillLineItem.java @@ -14,8 +14,9 @@ package org.openmrs.module.billing.api.model; import java.math.BigDecimal; +import java.util.Objects; -import org.openmrs.BaseOpenmrsData; +import org.openmrs.BaseChangeableOpenmrsData; import org.openmrs.Order; import org.openmrs.module.stockmanagement.api.model.StockItem; @@ -23,9 +24,9 @@ * A LineItem represents a line on a {@link Bill} which will bill some quantity of a particular * {@link StockItem}. */ -public class BillLineItem extends BaseOpenmrsData { +public class BillLineItem extends BaseChangeableOpenmrsData { - public static final long serialVersionUID = 0L; + private static final long serialVersionUID = 0L; private int billLineItemId; diff --git a/api/src/main/java/org/openmrs/module/billing/api/model/BillableService.java b/api/src/main/java/org/openmrs/module/billing/api/model/BillableService.java index f8098ca6..10a3b7e8 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/model/BillableService.java +++ b/api/src/main/java/org/openmrs/module/billing/api/model/BillableService.java @@ -16,12 +16,12 @@ import java.util.ArrayList; import java.util.List; -import org.openmrs.BaseOpenmrsData; +import org.openmrs.BaseChangeableOpenmrsMetadata; import org.openmrs.Concept; import org.openmrs.Location; import org.openmrs.Provider; -public class BillableService extends BaseOpenmrsData { +public class BillableService extends BaseChangeableOpenmrsMetadata { public static final long serialVersionUID = 0L; diff --git a/api/src/main/java/org/openmrs/module/billing/api/model/CashierItemPrice.java b/api/src/main/java/org/openmrs/module/billing/api/model/CashierItemPrice.java index 7023e831..43656eb4 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/model/CashierItemPrice.java +++ b/api/src/main/java/org/openmrs/module/billing/api/model/CashierItemPrice.java @@ -15,10 +15,10 @@ import java.math.BigDecimal; -import org.openmrs.BaseOpenmrsData; +import org.openmrs.BaseChangeableOpenmrsMetadata; import org.openmrs.module.stockmanagement.api.model.StockItem; -public class CashierItemPrice extends BaseOpenmrsData { +public class CashierItemPrice extends BaseChangeableOpenmrsMetadata { public static final long serialVersionUID = 0L; diff --git a/api/src/main/java/org/openmrs/module/billing/api/search/BillSearch.java b/api/src/main/java/org/openmrs/module/billing/api/search/BillSearch.java index 7c64d112..a70ded4e 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/search/BillSearch.java +++ b/api/src/main/java/org/openmrs/module/billing/api/search/BillSearch.java @@ -13,17 +13,29 @@ */ package org.openmrs.module.billing.api.search; +import java.util.List; + import org.hibernate.Criteria; import org.hibernate.criterion.Order; import org.hibernate.criterion.Restrictions; +import org.openmrs.Patient; +import org.openmrs.api.context.Context; +import org.openmrs.api.db.hibernate.HibernatePatientDAO; import org.openmrs.module.billing.api.base.entity.search.BaseDataTemplateSearch; import org.openmrs.module.billing.api.model.Bill; +import org.openmrs.module.billing.api.model.BillStatus; /** * A search template class for the {@link Bill} model. */ public class BillSearch extends BaseDataTemplateSearch { + private Boolean includeVoidedLineItems; + + private List statuses; + + private String patientName; + public BillSearch() { this(new Bill(), false); } @@ -34,6 +46,35 @@ public BillSearch(Bill template) { public BillSearch(Bill template, Boolean includeRetired) { super(template, includeRetired); + this.includeVoidedLineItems = false; + } + + /** + * Sets whether voided line items should be included in the results. + * + * @param includeVoidedLineItems {@code true} to include voided line items, {@code false} to exclude + * them. + * @return This BillSearch instance for method chaining. + */ + public BillSearch includeVoidedLineItems(boolean includeVoidedLineItems) { + this.includeVoidedLineItems = includeVoidedLineItems; + return this; + } + + public Boolean getIncludeVoidedLineItems() { + return includeVoidedLineItems; + } + + /** + * Sets multiple statuses to filter by. When multiple statuses are provided, bills matching any of + * the specified statuses will be returned. + * + * @param statuses The list of statuses to filter by. + * @return This BillSearch instance. + */ + public BillSearch setStatuses(List statuses) { + this.statuses = statuses; + return this; } @Override @@ -50,9 +91,31 @@ public void updateCriteria(Criteria criteria) { if (bill.getPatient() != null) { criteria.add(Restrictions.eq("patient", bill.getPatient())); } - if (bill.getStatus() != null) { + + if (patientName != null && !patientName.trim().isEmpty()) { + List matchingPatients = Context.getRegisteredComponent("patientDAO", HibernatePatientDAO.class) + .getPatients(patientName, 0, null); + if (matchingPatients != null && !matchingPatients.isEmpty()) { + criteria.add(Restrictions.in("patient", matchingPatients)); + } else { + criteria.add(Restrictions.sqlRestriction("1 = 2")); + } + } + + if (statuses != null && !statuses.isEmpty()) { + // Filter by multiple statuses using IN clause + criteria.add(Restrictions.in("status", statuses)); + } else if (bill.getStatus() != null) { criteria.add(Restrictions.eq("status", bill.getStatus())); } criteria.addOrder(Order.desc("id")); } + + public String getPatientName() { + return patientName; + } + + public void setPatientName(String patientName) { + this.patientName = patientName; + } } diff --git a/api/src/main/java/org/openmrs/module/billing/api/search/BillableServiceSearch.java b/api/src/main/java/org/openmrs/module/billing/api/search/BillableServiceSearch.java index 7891d066..3e08ea09 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/search/BillableServiceSearch.java +++ b/api/src/main/java/org/openmrs/module/billing/api/search/BillableServiceSearch.java @@ -16,10 +16,10 @@ import org.hibernate.Criteria; import org.hibernate.criterion.MatchMode; import org.hibernate.criterion.Restrictions; -import org.openmrs.module.billing.api.base.entity.search.BaseDataTemplateSearch; +import org.openmrs.module.billing.api.base.entity.search.BaseMetadataTemplateSearch; import org.openmrs.module.billing.api.model.BillableService; -public class BillableServiceSearch extends BaseDataTemplateSearch { +public class BillableServiceSearch extends BaseMetadataTemplateSearch { public BillableServiceSearch() { this(new BillableService(), false); diff --git a/api/src/main/java/org/openmrs/module/billing/util/Utils.java b/api/src/main/java/org/openmrs/module/billing/util/Utils.java index ad125e43..1fe88811 100644 --- a/api/src/main/java/org/openmrs/module/billing/util/Utils.java +++ b/api/src/main/java/org/openmrs/module/billing/util/Utils.java @@ -40,13 +40,9 @@ import org.openmrs.Concept; import org.openmrs.Encounter; import org.openmrs.EncounterType; -import org.openmrs.GlobalProperty; -import org.openmrs.Location; -import org.openmrs.LocationAttribute; import org.openmrs.Obs; import org.openmrs.Patient; import org.openmrs.api.context.Context; -import org.openmrs.util.PrivilegeConstants; public class Utils { diff --git a/api/src/main/resources/Bill.hbm.xml b/api/src/main/resources/Bill.hbm.xml index 597b9c06..18560dc5 100644 --- a/api/src/main/resources/Bill.hbm.xml +++ b/api/src/main/resources/Bill.hbm.xml @@ -29,10 +29,10 @@ - - - - + + + + @@ -61,6 +61,7 @@ + org.openmrs.module.billing.api.model.BillableServiceStatus @@ -74,11 +75,10 @@ - - - - - + + + + @@ -112,10 +112,12 @@ + + diff --git a/api/src/test/java/org/openmrs/module/billing/IBillServiceTest.java b/api/src/test/java/org/openmrs/module/billing/IBillServiceTest.java deleted file mode 100644 index 81af64e5..00000000 --- a/api/src/test/java/org/openmrs/module/billing/IBillServiceTest.java +++ /dev/null @@ -1,464 +0,0 @@ -///* -// * The contents of this file are subject to the OpenMRS Public License -// * Version 1.1 (the "License"); you may not use this file except in -// * compliance with the License. You may obtain a copy of the License at -// * http://license.openmrs.org -// * -// * Software distributed under the License is distributed on an "AS IS" -// * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the -// * License for the specific language governing rights and limitations -// * under the License. -// * -// * Copyright (C) OpenMRS, LLC. All Rights Reserved. -// */ -//package org.openmrs.module.cashier.api; -// -//import java.math.BigDecimal; -//import java.util.Iterator; -//import java.util.List; -//import java.util.Set; -// -////import liquibase.util.StringUtils; -// -//import org.junit.Assert; -//import org.junit.Test; -//import org.openmrs.Patient; -//import org.openmrs.api.PatientService; -//import org.openmrs.api.ProviderService; -//import org.openmrs.api.context.Context; -//import org.openmrs.module.cashier.api.base.PagingInfo; -//import org.openmrs.module.cashier.api.model.Bill; -//import org.openmrs.module.cashier.api.model.BillLineItem; -//import org.openmrs.module.cashier.api.model.BillStatus; -//import org.openmrs.module.cashier.api.model.CashPoint; -//import org.openmrs.module.cashier.api.model.Payment; -//import org.openmrs.module.cashier.api.model.PaymentAttribute; -//import org.openmrs.module.cashier.api.model.PaymentMode; -//import org.openmrs.module.cashier.api.search.BillSearch; -//import org.openmrs.module.cashier.api.base.entity.IEntityDataServiceTest; -//import org.openmrs.module.openhmis.inventory.api.IItemDataService; -//import org.openmrs.module.openhmis.inventory.api.IItemDataServiceTest; -//import org.openmrs.module.openhmis.inventory.api.model.Item; -// -//public abstract class IBillServiceTest extends IEntityDataServiceTest { -// public static final String BILL_DATASET = TestConstants.BASE_DATASET_DIR + "BillTest.xml"; -// -// private ProviderService providerService; -// private PatientService patientService; -// private IItemDataService itemService; -// private IPaymentModeService paymentModeService; -// private IPaymentModeAttributeTypeService paymentModeAttributeTypeService; -// private ICashPointService cashPointService; -// -// @Override -// public void before() throws Exception { -// super.before(); -// -// providerService = Context.getProviderService(); -// patientService = Context.getPatientService(); -// itemService = Context.getService(IItemDataService.class); -// paymentModeService = Context.getService(IPaymentModeService.class); -// paymentModeAttributeTypeService = Context.getService(IPaymentModeAttributeTypeService.class); -// cashPointService = Context.getService(ICashPointService.class); -// -// executeDataSet(IItemDataServiceTest.ITEM_DATASET); -// executeDataSet(IPaymentModeServiceTest.PAYMENT_MODE_DATASET); -// executeDataSet(ICashPointServiceTest.CASH_POINT_DATASET); -// executeDataSet(TestConstants.CORE_DATASET); -// executeDataSet(BILL_DATASET); -// } -// -// @Override -// public Bill createEntity(boolean valid) { -// Bill bill = new Bill(); -// -// if (valid) { -// bill.setCashier(providerService.getProvider(0)); -// bill.setPatient(patientService.getPatient(0)); -// bill.setCashPoint(cashPointService.getById(0)); -// bill.setReceiptNumber("Test 1234"); -// bill.setStatus(BillStatus.PAID); -// } -// -// Item item = itemService.getById(0); -// bill.addLineItem(item, item.getPrices().iterator().next(), 1); -// item = itemService.getById(1); -// bill.addLineItem(item, item.getPrices().iterator().next(), 1); -// -// PaymentMode mode = paymentModeService.getById(0); -// Payment payment = bill.addPayment(mode, null, BigDecimal.valueOf(100), BigDecimal.valueOf(100)); -// payment.addAttribute(paymentModeAttributeTypeService.getById(0), "test"); -// payment.addAttribute(paymentModeAttributeTypeService.getById(1), "test2"); -// payment.addAttribute(paymentModeAttributeTypeService.getById(2), "test3"); -// -// mode = paymentModeService.getById(1); -// bill.addPayment(mode, null, BigDecimal.valueOf(200), BigDecimal.valueOf(200)); -// -// return bill; -// } -// -// @Override -// protected int getTestEntityCount() { -// return 1; -// } -// -// @Override -// protected void updateEntityFields(Bill bill) { -// bill.setCashier(providerService.getProvider(1)); -// bill.setPatient(patientService.getPatient(2)); -// bill.setCashPoint(cashPointService.getById(0)); -// bill.setReceiptNumber(bill.getReceiptNumber() + " updated"); -// bill.setStatus(BillStatus.PENDING); -// -// List lines = bill.getLineItems(); -// if (lines.size() > 0) { -// BillLineItem item = lines.get(0); -// -// item.setPrice(item.getPrice().multiply(BigDecimal.valueOf(2))); -// item.setPriceName(item.getPriceName() + " updated"); -// -// if (lines.size() > 1) { -// item = lines.get(1); -// -// bill.removeLineItem(item); -// } -// } -// -// Item newItem = itemService.getById(2); -// bill.addLineItem(newItem, newItem.getPrices().iterator().next(), 3); -// -// Set payments = bill.getPayments(); -// if (payments.size() > 0) { -// Iterator iterator = payments.iterator(); -// -// Payment payment = iterator.next(); -// payment.setAmount(payment.getAmount().divide(BigDecimal.valueOf(2))); -// -// if (payments.size() > 1) { -// payment = iterator.next(); -// -// bill.removePayment(payment); -// } -// } -// -// bill.addPayment(paymentModeService.getById(2), null, BigDecimal.valueOf(303.11), BigDecimal.valueOf(350.00)); -// } -// -// @Override -// protected void assertEntity(Bill expected, Bill actual) { -// super.assertEntity(expected, actual); -// -// Assert.assertNotNull(expected.getCashier()); -// Assert.assertNotNull(actual.getCashier()); -// Assert.assertEquals(expected.getCashier().getId(), actual.getCashier().getId()); -// Assert.assertNotNull(expected.getPatient()); -// Assert.assertNotNull(actual.getPatient()); -// Assert.assertEquals(expected.getPatient().getId(), actual.getPatient().getId()); -// Assert.assertNotNull(expected.getCashPoint()); -// Assert.assertNotNull(actual.getCashPoint()); -// Assert.assertEquals(expected.getCashPoint().getId(), actual.getCashPoint().getId()); -// -// Assert.assertEquals(expected.getReceiptNumber(), actual.getReceiptNumber()); -// Assert.assertEquals(expected.getStatus(), actual.getStatus()); -// -// if (expected.getLineItems() == null) { -// Assert.assertNull(actual.getLineItems()); -// } else { -// Assert.assertEquals(expected.getLineItems().size(), actual.getLineItems().size()); -// BillLineItem[] expectedItems = new BillLineItem[expected.getLineItems().size()]; -// expected.getLineItems().toArray(expectedItems); -// BillLineItem[] actualItems = new BillLineItem[actual.getLineItems().size()]; -// actual.getLineItems().toArray(actualItems); -// for (int i = 0; i < expected.getLineItems().size(); i++) { -// Assert.assertEquals(expectedItems[i].getId(), actualItems[i].getId()); -// Assert.assertEquals(expectedItems[i].getItem(), actualItems[i].getItem()); -// Assert.assertEquals(expectedItems[i].getPrice(), actualItems[i].getPrice()); -// Assert.assertEquals(expectedItems[i].getPriceName(), actualItems[i].getPriceName()); -// Assert.assertEquals(expectedItems[i].getQuantity(), actualItems[i].getQuantity()); -// Assert.assertEquals(expectedItems[i].getUuid(), actualItems[i].getUuid()); -// } -// } -// -// if (expected.getPayments() == null) { -// Assert.assertNull(actual.getPayments()); -// } else { -// Assert.assertEquals(expected.getPayments().size(), actual.getPayments().size()); -// Payment[] expectedPayments = new Payment[expected.getPayments().size()]; -// expected.getPayments().toArray(expectedPayments); -// Payment[] actualPayments = new Payment[actual.getPayments().size()]; -// actual.getPayments().toArray(actualPayments); -// for (int i = 0; i < expected.getPayments().size(); i++) { -// Assert.assertEquals(expectedPayments[i].getId(), actualPayments[i].getId()); -// Assert.assertEquals(expectedPayments[i].getInstanceType(), actualPayments[i].getInstanceType()); -// Assert.assertEquals(expectedPayments[i].getAmount(), actualPayments[i].getAmount()); -// Assert.assertEquals(expectedPayments[i].getUuid(), actualPayments[i].getUuid()); -// -// if (expectedPayments[i].getAttributes() == null) { -// Assert.assertNull(actualPayments[i].getAttributes()); -// } else { -// Assert.assertEquals(expectedPayments[i].getAttributes().size(), actualPayments[i].getAttributes() -// .size()); -// if (expectedPayments[i].getAttributes().size() > 0) { -// PaymentAttribute[] expectedAttributes = -// new PaymentAttribute[expectedPayments[i].getAttributes().size()]; -// expectedPayments[i].getAttributes().toArray(expectedAttributes); -// PaymentAttribute[] actualAttributes = -// new PaymentAttribute[actualPayments[i].getAttributes().size()]; -// actualPayments[i].getAttributes().toArray(actualAttributes); -// for (int j = 0; j < expectedAttributes.length; j++) { -// Assert.assertEquals(expectedAttributes[j].getId(), actualAttributes[j].getId()); -// Assert.assertEquals(expectedAttributes[j].getValue(), actualAttributes[j].getValue()); -// Assert.assertEquals(expectedAttributes[j].getAttributeType(), -// actualAttributes[j].getAttributeType()); -// Assert.assertEquals(expectedAttributes[j].getUuid(), actualAttributes[j].getUuid()); -// } -// } -// } -// } -// } -// } -// -// /** -// * @verifies throw IllegalArgumentException if the receipt number is null -// * @see IBillService#getBillByReceiptNumber(String) -// */ -// @Test(expected = IllegalArgumentException.class) -// public void getBillByReceiptNumber_shouldThrowIllegalArgumentExceptionIfTheReceiptNumberIsNull() throws Exception { -// service.getBillByReceiptNumber(null); -// } -// -// /** -// * @verifies throw IllegalArgumentException if the receipt number is empty -// * @see IBillService#getBillByReceiptNumber(String) -// */ -// @Test(expected = IllegalArgumentException.class) -// public void getBillByReceiptNumber_shouldThrowIllegalArgumentExceptionIfTheReceiptNumberIsEmpty() throws Exception { -// service.getBillByReceiptNumber(""); -// } -// -// /** -// * @verifies throw IllegalArgumentException if the receipt number is longer than 255 characters -// * @see IBillService#getBillByReceiptNumber(String) -// */ -// @Test(expected = IllegalArgumentException.class) -// public void getBillByReceiptNumber_shouldThrowIllegalArgumentExceptionIfTheReceiptNumberIsLongerThan255Characters() -// throws Exception { -// // service.getBillByReceiptNumber(StringUtils.repeat("A", 256)); -// } -// -// /** -// * @verifies return the bill with the specified reciept number -// * @see IBillService#getBillByReceiptNumber(String) -// */ -// @Test -// public void getBillByReceiptNumber_shouldReturnTheBillWithTheSpecifiedRecieptNumber() throws Exception { -// Bill bill = service.getBillByReceiptNumber("test 1 receipt number"); -// Assert.assertNotNull(bill); -// -// Bill expected = service.getById(0); -// -// assertEntity(expected, bill); -// } -// -// /** -// * @verifies return null if the receipt number is not found -// * @see IBillService#getBillByReceiptNumber(String) -// */ -// @Test -// public void getBillByReceiptNumber_shouldReturnNullIfTheReceiptNumberIsNotFound() throws Exception { -// Bill bill = service.getBillByReceiptNumber("not a valid number"); -// -// Assert.assertNull(bill); -// } -// -// @Test -// public void save_adjustedBill() throws Exception { -// Bill bill = createEntity(true); -// bill.setBillAdjusted(service.getById(0)); -// service.save(bill); -// -// Context.flushSession(); -// -// bill = service.getById(bill.getId()); -// Assert.assertNotNull(bill); -// Assert.assertNotNull(bill.getBillAdjusted()); -// -// Bill adjustedBill = service.getById(bill.getBillAdjusted().getId()); -// Assert.assertNotNull(adjustedBill); -// Assert.assertEquals(BillStatus.ADJUSTED, adjustedBill.getStatus()); -// Assert.assertTrue(adjustedBill.getAdjustedBy().size() > 0); -// -// boolean foundAdjustor = false; -// for (Bill adjustor : adjustedBill.getAdjustedBy()) { -// if (adjustor.getId() == bill.getId()) { -// foundAdjustor = true; -// break; -// } -// } -// -// Assert.assertTrue("Could not find the adjusting bill.", foundAdjustor); -// } -// -// /** -// * @verifies throw NullPointerException if patient is null -// * @see IBillService#getBillsByPatient(org.openmrs.Patient, PagingInfo) -// */ -// @Test(expected = NullPointerException.class) -// public void getBillsByPatient_shouldThrowNullPointerExceptionIfPatientIsNull() throws Exception { -// service.getBillsByPatient(null, null); -// } -// -// /** -// * @verifies return all bills for the specified patient -// * @see IBillService#getBillsByPatient(org.openmrs.Patient, PagingInfo) -// */ -// @Test -// public void getBillsByPatientId_shouldReturnAllBillsForTheSpecifiedPatient() throws Exception { -// Patient patient = patientService.getPatient(0); -// -// List bills = service.getBillsByPatient(patient, null); -// -// Assert.assertNotNull(bills); -// Assert.assertEquals(1, bills.size()); -// assertEntity(service.getById(0), bills.get(0)); -// -// bills = service.getBillsByPatientId(patient.getId(), null); -// Assert.assertNotNull(bills); -// Assert.assertEquals(1, bills.size()); -// assertEntity(service.getById(0), bills.get(0)); -// } -// -// /** -// * @verifies return an empty list if the specified patient has no bills -// * @see IBillService#getBillsByPatient(org.openmrs.Patient, PagingInfo) -// */ -// @Test -// public void getBillsByPatientId_shouldReturnAnEmptyListIfTheSpecifiedPatientHasNoBills() throws Exception { -// Patient patient = patientService.getPatient(1); -// -// List bills = service.getBillsByPatient(patient, null); -// Assert.assertNotNull(bills); -// Assert.assertEquals(0, bills.size()); -// -// bills = service.getBillsByPatientId(1, null); -// Assert.assertNotNull(bills); -// Assert.assertEquals(0, bills.size()); -// } -// -// /** -// * @verifies throw IllegalArgumentException if the patientId is less than zero -// * @see IBillService#getBillsByPatientId(int, PagingInfo) -// */ -// @Test(expected = IllegalArgumentException.class) -// public void getBillsByPatientId_shouldThrowIllegalArgumentExceptionIfThePatientIdIsLessThanZero() throws Exception { -// service.getBillsByPatientId(-1, null); -// } -// -// /** -// * @verifies throw NullPointerException if bill search is null -// * @see IBillService#getBills(BillSearch, PagingInfo) -// */ -// @Test(expected = NullPointerException.class) -// public void getBills_throwNullPointerExceptionIfBillSearchIsNull() throws Exception { -// service.getBills(null, null); -// } -// -// /** -// * @verifies throw NullPointerException if bill search template object is null -// * @see IBillService#getBills(BillSearch, PagingInfo) -// */ -// @Test(expected = NullPointerException.class) -// public void getBills_throwNullPointerExceptionIfBillSearchTemplateObjectIsNull() throws Exception { -// BillSearch search = new BillSearch(); -// search.setTemplate(null); -// service.getBills(search, null); -// } -// -// /** -// * @verifies return an empty list if no bills are found via the search -// * @see IBillService#getBills(BillSearch, PagingInfo) -// */ -// @Test -// public void getBills_returnAnEmptyListIfNoBillsAreFoundViaTheSearch() throws Exception { -// BillSearch billSearch = new BillSearch(); -// Bill bill = new Bill(); -// CashPoint cashPoint = new CashPoint(); -// cashPoint.setId(2); -// bill.setCashPoint(cashPoint); -// billSearch.setTemplate(bill); -// List results = service.getBills(billSearch, null); -// Assert.assertTrue(results.isEmpty()); -// } -// -// /** -// * @verifies return bills filtered by cashier -// * @see IBillService#getBills(BillSearch, PagingInfo) -// */ -// @Test -// public void getBills_returnBillsFilteredByCashier() throws Exception { -// Bill bill = new Bill(); -// bill.setCashier(providerService.getProvider(0)); -// List results = service.getBills(new BillSearch(bill), null); -// Assert.assertEquals(1, results.size()); -// } -// -// /** -// * @verifies return bills filtered by cash point -// * @see IBillService#getBills(BillSearch, PagingInfo) -// */ -// @Test -// public void getBills_returnBillsFilteredByCashPoint() throws Exception { -// Bill bill = new Bill(); -// bill.setCashPoint(cashPointService.getById(0)); -// List results = service.getBills(new BillSearch(bill), null); -// Assert.assertEquals(1, results.size()); -// } -// -// /** -// * @verifies return bills filtered by patient -// * @see IBillService#getBills(BillSearch, PagingInfo) -// */ -// @Test -// public void getBills_returnBillsFilteredByPatient() throws Exception { -// Bill bill = new Bill(); -// bill.setPatient(patientService.getPatient(0)); -// List results = service.getBills(new BillSearch(bill), null); -// Assert.assertEquals(1, results.size()); -// } -// -// /** -// * @verifies return bills filtered by status -// * @see IBillService#getBills(BillSearch, PagingInfo) -// */ -// @Test -// public void getBills_returnBillsFilteredByStatus() throws Exception { -// Bill bill = new Bill(); -// bill.setStatus(BillStatus.POSTED); -// List results = service.getBills(new BillSearch(bill), null); -// Assert.assertEquals(1, results.size()); -// } -// -// /** -// * @verifies return all bills if paging is null -// * @see IBillService#getBills(BillSearch, PagingInfo) -// */ -// @Test -// public void getBills_returnAllBillsIfPagingIsNull() throws Exception { -// List results = service.getBills(new BillSearch(), null); -// Assert.assertEquals(1, results.size()); -// } -// -// /** -// * @verifies return paged bills if paging is specified -// * @see IBillService#getBills(BillSearch, org.openmrs.module.cashier.api.base.PagingInfo) -// */ -// @Test -// public void getBills_returnPagedBillsIfPagingIsSpecified() throws Exception { -// PagingInfo pagingInfo = new PagingInfo(1, 100); -// List results = service.getBills(new BillSearch(), pagingInfo); -// -// Assert.assertNotNull(results); -// Assert.assertEquals(1, results.size()); -// Assert.assertEquals(1, (long)pagingInfo.getTotalRecordCount()); -// } -//} diff --git a/api/src/test/java/org/openmrs/module/billing/TestConstants.java b/api/src/test/java/org/openmrs/module/billing/TestConstants.java index 9595cdc7..52b84680 100644 --- a/api/src/test/java/org/openmrs/module/billing/TestConstants.java +++ b/api/src/test/java/org/openmrs/module/billing/TestConstants.java @@ -18,4 +18,6 @@ public class TestConstants { public static final String BASE_DATASET_DIR = "org/openmrs/module/billing/api/include/"; public static final String CORE_DATASET = BASE_DATASET_DIR + "CoreTest.xml"; + + public static final String CORE_DATASET2 = BASE_DATASET_DIR + "CoreTest-2.0.xml"; } diff --git a/api/src/test/java/org/openmrs/module/billing/api/model/BillTest.java b/api/src/test/java/org/openmrs/module/billing/api/model/BillTest.java new file mode 100644 index 00000000..f34f508a --- /dev/null +++ b/api/src/test/java/org/openmrs/module/billing/api/model/BillTest.java @@ -0,0 +1,327 @@ +package org.openmrs.module.billing.api.model; + +import static org.junit.Assert.assertEquals; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.HashSet; + +import org.junit.Test; + +/** + * Test for verifying Bill model methods, particularly getTotalPayments() + */ +public class BillTest { + + @Test + public void getTotalPayments_shouldExcludeVoidedPaymentsFromTotal() { + Bill bill = new Bill(); + bill.setPayments(new HashSet<>()); + + Payment validPayment1 = new Payment(); + validPayment1.setAmountTendered(BigDecimal.valueOf(50)); + validPayment1.setVoided(false); + bill.getPayments().add(validPayment1); + + Payment validPayment2 = new Payment(); + validPayment2.setAmountTendered(BigDecimal.valueOf(30)); + validPayment2.setVoided(false); + bill.getPayments().add(validPayment2); + + Payment voidedPayment1 = new Payment(); + voidedPayment1.setAmountTendered(BigDecimal.valueOf(20)); + voidedPayment1.setVoided(true); + bill.getPayments().add(voidedPayment1); + + Payment voidedPayment2 = new Payment(); + voidedPayment2.setAmountTendered(BigDecimal.valueOf(40)); + voidedPayment2.setVoided(true); + bill.getPayments().add(voidedPayment2); + + assertEquals(BigDecimal.valueOf(80), bill.getTotalPayments()); + } + + @Test + public void getTotalPayments_shouldReturnZeroWhenAllPaymentsAreVoided() { + Bill bill = new Bill(); + bill.setPayments(new HashSet<>()); + + Payment voidedPayment = new Payment(); + voidedPayment.setAmountTendered(BigDecimal.valueOf(100)); + voidedPayment.setVoided(true); + bill.getPayments().add(voidedPayment); + + assertEquals(BigDecimal.ZERO, bill.getTotalPayments()); + } + + @Test + public void getTotal_shouldExcludeVoidedLineItemsFromTotal() { + Bill bill = new Bill(); + bill.setLineItems(new ArrayList<>()); + + BillLineItem lineItem1 = new BillLineItem(); + lineItem1.setPrice(BigDecimal.valueOf(100)); + lineItem1.setQuantity(2); + lineItem1.setVoided(false); + bill.getLineItems().add(lineItem1); + + BillLineItem lineItem2 = new BillLineItem(); + lineItem2.setPrice(BigDecimal.valueOf(50)); + lineItem2.setQuantity(1); + lineItem2.setVoided(false); + bill.getLineItems().add(lineItem2); + + BillLineItem voidedLineItem1 = new BillLineItem(); + voidedLineItem1.setPrice(BigDecimal.valueOf(75)); + voidedLineItem1.setQuantity(3); + voidedLineItem1.setVoided(true); + bill.getLineItems().add(voidedLineItem1); + + BillLineItem voidedLineItem2 = new BillLineItem(); + voidedLineItem2.setPrice(BigDecimal.valueOf(30)); + voidedLineItem2.setQuantity(2); + voidedLineItem2.setVoided(true); + bill.getLineItems().add(voidedLineItem2); + + assertEquals(BigDecimal.valueOf(250), bill.getTotal()); + } + + @Test + public void getTotal_shouldReturnZeroWhenAllLineItemsAreVoided() { + Bill bill = new Bill(); + bill.setLineItems(new ArrayList<>()); + + BillLineItem voidedLineItem = new BillLineItem(); + voidedLineItem.setPrice(BigDecimal.valueOf(100)); + voidedLineItem.setQuantity(5); + voidedLineItem.setVoided(true); + bill.getLineItems().add(voidedLineItem); + + assertEquals(BigDecimal.ZERO, bill.getTotal()); + } + + @Test + public void synchronizeBillStatus_shouldUpdateStatusToPaidWhenFullyPaid() { + Bill bill = new Bill(); + bill.setLineItems(new ArrayList<>()); + bill.setPayments(new HashSet<>()); + + BillLineItem lineItem = new BillLineItem(); + lineItem.setPrice(BigDecimal.valueOf(100)); + lineItem.setQuantity(1); + lineItem.setVoided(false); + bill.getLineItems().add(lineItem); + + Payment payment = new Payment(); + payment.setAmountTendered(BigDecimal.valueOf(100)); + payment.setVoided(false); + bill.getPayments().add(payment); + + bill.synchronizeBillStatus(); + + assertEquals(BillStatus.PAID, bill.getStatus()); + } + + @Test + public void synchronizeBillStatus_shouldUpdateStatusToPostedWhenPartiallyPaid() { + Bill bill = new Bill(); + bill.setLineItems(new ArrayList<>()); + bill.setPayments(new HashSet<>()); + + BillLineItem lineItem = new BillLineItem(); + lineItem.setPrice(BigDecimal.valueOf(100)); + lineItem.setQuantity(1); + lineItem.setVoided(false); + bill.getLineItems().add(lineItem); + + Payment payment = new Payment(); + payment.setAmountTendered(BigDecimal.valueOf(50)); + payment.setVoided(false); + bill.getPayments().add(payment); + + bill.synchronizeBillStatus(); + + assertEquals(BillStatus.POSTED, bill.getStatus()); + } + + @Test + public void synchronizeBillStatus_shouldUpdateStatusToPaidAfterVoidingLineItems() { + Bill bill = new Bill(); + bill.setLineItems(new ArrayList<>()); + bill.setPayments(new HashSet<>()); + + BillLineItem lineItem1 = new BillLineItem(); + lineItem1.setPrice(BigDecimal.valueOf(100)); + lineItem1.setQuantity(1); + lineItem1.setVoided(false); + bill.getLineItems().add(lineItem1); + + BillLineItem lineItem2 = new BillLineItem(); + lineItem2.setPrice(BigDecimal.valueOf(50)); + lineItem2.setQuantity(1); + lineItem2.setVoided(false); + bill.getLineItems().add(lineItem2); + + Payment payment = new Payment(); + payment.setAmountTendered(BigDecimal.valueOf(100)); + payment.setVoided(false); + bill.getPayments().add(payment); + + bill.synchronizeBillStatus(); + assertEquals(BillStatus.POSTED, bill.getStatus()); + + lineItem2.setVoided(true); + + bill.synchronizeBillStatus(); + assertEquals(BillStatus.PAID, bill.getStatus()); + } + + @Test + public void addLineItem_shouldAllowAddingLineItemToNewBill() { + Bill bill = new Bill(); + bill.setStatus(BillStatus.PENDING); + + BillLineItem lineItem = new BillLineItem(); + lineItem.setPrice(BigDecimal.valueOf(100)); + lineItem.setQuantity(1); + + // Should not throw exception for new bill (no ID) + bill.addLineItem(lineItem); + assertEquals(1, bill.getLineItems().size()); + } + + @Test + public void addLineItem_shouldAllowAddingLineItemToExistingPendingBill() { + Bill bill = new Bill(); + bill.setId(1); + bill.setStatus(BillStatus.PENDING); + bill.setLineItems(new ArrayList<>()); + + BillLineItem lineItem = new BillLineItem(); + lineItem.setPrice(BigDecimal.valueOf(100)); + lineItem.setQuantity(1); + + // Should not throw exception for PENDING bill + bill.addLineItem(lineItem); + assertEquals(1, bill.getLineItems().size()); + } + + @Test(expected = IllegalStateException.class) + public void addLineItem_shouldThrowExceptionWhenBillIsPosted() { + Bill bill = new Bill(); + bill.setId(1); + bill.setStatus(BillStatus.POSTED); + bill.setLineItems(new ArrayList<>()); + + BillLineItem lineItem = new BillLineItem(); + lineItem.setPrice(BigDecimal.valueOf(100)); + lineItem.setQuantity(1); + + bill.addLineItem(lineItem); + } + + @Test(expected = IllegalStateException.class) + public void addLineItem_shouldThrowExceptionWhenBillIsPaid() { + Bill bill = new Bill(); + bill.setId(1); + bill.setStatus(BillStatus.PAID); + bill.setLineItems(new ArrayList<>()); + + BillLineItem lineItem = new BillLineItem(); + lineItem.setPrice(BigDecimal.valueOf(100)); + lineItem.setQuantity(1); + + bill.addLineItem(lineItem); + } + + @Test(expected = IllegalStateException.class) + public void addLineItem_shouldThrowExceptionWhenBillIsCancelled() { + Bill bill = new Bill(); + bill.setId(1); + bill.setStatus(BillStatus.CANCELLED); + bill.setLineItems(new ArrayList<>()); + + BillLineItem lineItem = new BillLineItem(); + lineItem.setPrice(BigDecimal.valueOf(100)); + lineItem.setQuantity(1); + + bill.addLineItem(lineItem); + } + + @Test + public void removeLineItem_shouldAllowRemovingLineItemFromPendingBill() { + Bill bill = new Bill(); + bill.setId(1); + bill.setStatus(BillStatus.PENDING); + bill.setLineItems(new ArrayList<>()); + + BillLineItem lineItem = new BillLineItem(); + lineItem.setPrice(BigDecimal.valueOf(100)); + lineItem.setQuantity(1); + bill.getLineItems().add(lineItem); + + // Should not throw exception for PENDING bill + bill.removeLineItem(lineItem); + assertEquals(0, bill.getLineItems().size()); + } + + @Test(expected = IllegalStateException.class) + public void removeLineItem_shouldThrowExceptionWhenBillIsPosted() { + Bill bill = new Bill(); + bill.setId(1); + bill.setStatus(BillStatus.POSTED); + bill.setLineItems(new ArrayList<>()); + + BillLineItem lineItem = new BillLineItem(); + lineItem.setPrice(BigDecimal.valueOf(100)); + lineItem.setQuantity(1); + bill.getLineItems().add(lineItem); + + bill.removeLineItem(lineItem); + } + + @Test + public void setLineItems_shouldAllowSettingLineItemsOnNewBill() { + Bill bill = new Bill(); + bill.setStatus(BillStatus.PENDING); + + ArrayList lineItems = new ArrayList<>(); + BillLineItem lineItem = new BillLineItem(); + lineItem.setPrice(BigDecimal.valueOf(100)); + lineItem.setQuantity(1); + lineItems.add(lineItem); + + // Should not throw exception for new bill (no ID) + bill.setLineItems(lineItems); + assertEquals(1, bill.getLineItems().size()); + } + + @Test + public void setLineItems_shouldAllowSettingLineItemsOnExistingPendingBill() { + Bill bill = new Bill(); + bill.setId(1); + bill.setStatus(BillStatus.PENDING); + + ArrayList lineItems = new ArrayList<>(); + BillLineItem lineItem = new BillLineItem(); + lineItem.setPrice(BigDecimal.valueOf(100)); + lineItem.setQuantity(1); + lineItems.add(lineItem); + + // Should not throw exception for PENDING bill + bill.setLineItems(lineItems); + assertEquals(1, bill.getLineItems().size()); + } + + @Test(expected = IllegalStateException.class) + public void setLineItems_shouldThrowExceptionWhenBillIsPosted() { + Bill bill = new Bill(); + bill.setId(1); + bill.setStatus(BillStatus.POSTED); + ArrayList existingLineItems = new ArrayList<>(); + bill.setLineItems(existingLineItems); + existingLineItems.add(new BillLineItem()); + bill.setLineItems(existingLineItems); + } + +} diff --git a/api/src/test/java/org/openmrs/module/billing/impl/BillLineItemServiceImplTest.java b/api/src/test/java/org/openmrs/module/billing/impl/BillLineItemServiceImplTest.java new file mode 100644 index 00000000..cd6e0151 --- /dev/null +++ b/api/src/test/java/org/openmrs/module/billing/impl/BillLineItemServiceImplTest.java @@ -0,0 +1,195 @@ +/* + * The contents of this file are subject to the OpenMRS Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://license.openmrs.org + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the + * License for the specific language governing rights and limitations + * under the License. + * + * Copyright (C) OpenMRS, LLC. All Rights Reserved. + */ + +package org.openmrs.module.billing.impl; + +import static org.junit.jupiter.api.Assertions.*; + +import java.math.BigDecimal; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openmrs.api.context.Context; +import org.openmrs.module.billing.TestConstants; +import org.openmrs.module.billing.api.BillLineItemService; +import org.openmrs.module.billing.api.IBillService; +import org.openmrs.module.billing.api.model.Bill; +import org.openmrs.module.billing.api.model.BillLineItem; +import org.openmrs.module.billing.api.model.BillStatus; +import org.openmrs.test.jupiter.BaseModuleContextSensitiveTest; + +public class BillLineItemServiceImplTest extends BaseModuleContextSensitiveTest { + + private BillLineItemService billLineItemService; + + private IBillService billService; + + @BeforeEach + public void setup() { + billLineItemService = Context.getService(BillLineItemService.class); + billService = Context.getService(IBillService.class); + executeDataSet(TestConstants.CORE_DATASET2); + executeDataSet(TestConstants.BASE_DATASET_DIR + "StockOperationType.xml"); + executeDataSet(TestConstants.BASE_DATASET_DIR + "PaymentModeTest.xml"); + executeDataSet(TestConstants.BASE_DATASET_DIR + "CashPointTest.xml"); + executeDataSet(TestConstants.BASE_DATASET_DIR + "BillTest.xml"); + } + + /** + * @see org.openmrs.module.billing.api.impl.BillLineItemServiceImpl#save(BillLineItem) + */ + @Test + public void save_shouldAllowSavingLineItemForPendingBill() { + // Get the PENDING bill from test data (bill_id=2) + Bill pendingBill = billService.getById(2); + assertNotNull(pendingBill); + assertEquals(BillStatus.PENDING, pendingBill.getStatus()); + + // Get a line item from the pending bill + BillLineItem lineItem = pendingBill.getLineItems().get(0); + assertNotNull(lineItem); + + // Update the line item + lineItem.setPrice(BigDecimal.valueOf(99.99)); + + // Should not throw exception + BillLineItem savedItem = billLineItemService.save(lineItem); + assertNotNull(savedItem); + assertEquals(BigDecimal.valueOf(99.99), savedItem.getPrice()); + } + + /** + * @see org.openmrs.module.billing.api.impl.BillLineItemServiceImpl#save(BillLineItem) + */ + @Test + public void save_shouldThrowExceptionWhenSavingLineItemForPostedBill() { + // Get the POSTED bill from test data (bill_id=0) + Bill postedBill = billService.getById(0); + assertNotNull(postedBill); + assertEquals(BillStatus.POSTED, postedBill.getStatus()); + + // Get a line item from the posted bill + BillLineItem lineItem = postedBill.getLineItems().get(0); + assertNotNull(lineItem); + + // Try to update the line item + lineItem.setPrice(BigDecimal.valueOf(99.99)); + + // Should throw exception + assertThrows(IllegalStateException.class, () -> billLineItemService.save(lineItem)); + } + + /** + * @see org.openmrs.module.billing.api.impl.BillLineItemServiceImpl#save(BillLineItem) + */ + @Test + public void save_shouldThrowExceptionWhenSavingLineItemForPaidBill() { + // Get the PAID bill from test data (bill_id=1) + Bill paidBill = billService.getById(1); + assertNotNull(paidBill); + assertEquals(BillStatus.PAID, paidBill.getStatus()); + + // Get a line item from the paid bill + BillLineItem lineItem = paidBill.getLineItems().get(0); + assertNotNull(lineItem); + + // Try to update the line item + lineItem.setPrice(BigDecimal.valueOf(99.99)); + + // Should throw exception + assertThrows(IllegalStateException.class, () -> billLineItemService.save(lineItem)); + } + + /** + * @see org.openmrs.module.billing.api.impl.BillLineItemServiceImpl#voidEntity(BillLineItem, String) + */ + @Test + public void voidEntity_shouldAllowVoidingLineItemForPendingBill() { + // Get the PENDING bill from test data (bill_id=2) + Bill pendingBill = billService.getById(2); + assertNotNull(pendingBill); + assertEquals(BillStatus.PENDING, pendingBill.getStatus()); + + // Get a line item from the pending bill + BillLineItem lineItem = pendingBill.getLineItems().get(0); + assertNotNull(lineItem); + assertFalse(lineItem.getVoided()); + + // Should not throw exception + BillLineItem voidedItem = billLineItemService.voidEntity(lineItem, "Test void reason"); + assertNotNull(voidedItem); + assertTrue(voidedItem.getVoided()); + } + + /** + * @see org.openmrs.module.billing.api.impl.BillLineItemServiceImpl#voidEntity(BillLineItem, String) + */ + @Test + public void voidEntity_shouldThrowExceptionWhenVoidingLineItemForPostedBill() { + // Get the POSTED bill from test data (bill_id=0) + Bill postedBill = billService.getById(0); + assertNotNull(postedBill); + assertEquals(BillStatus.POSTED, postedBill.getStatus()); + + // Get a line item from the posted bill + BillLineItem lineItem = postedBill.getLineItems().get(0); + assertNotNull(lineItem); + + // Should throw exception + assertThrows(IllegalStateException.class, () -> billLineItemService.voidEntity(lineItem, "Test void reason")); + } + + /** + * @see org.openmrs.module.billing.api.impl.BillLineItemServiceImpl#purge(BillLineItem) + */ + @Test + public void purge_shouldAllowPurgingLineItemForPendingBill() { + // Get the PENDING bill from test data (bill_id=2) + Bill pendingBill = billService.getById(2); + assertNotNull(pendingBill); + assertEquals(BillStatus.PENDING, pendingBill.getStatus()); + + int originalSize = pendingBill.getLineItems().size(); + assertTrue(originalSize > 0); + + // Get a line item from the pending bill + BillLineItem lineItem = pendingBill.getLineItems().get(0); + assertNotNull(lineItem); + + // Should not throw exception + billLineItemService.purge(lineItem); + + // Verify the line item was purged + Bill updatedBill = billService.getById(2); + assertTrue(updatedBill.getLineItems().size() < originalSize); + } + + /** + * @see org.openmrs.module.billing.api.impl.BillLineItemServiceImpl#purge(BillLineItem) + */ + @Test + public void purge_shouldThrowExceptionWhenPurgingLineItemForPostedBill() { + // Get the POSTED bill from test data (bill_id=0) + Bill postedBill = billService.getById(0); + assertNotNull(postedBill); + assertEquals(BillStatus.POSTED, postedBill.getStatus()); + + // Get a line item from the posted bill + BillLineItem lineItem = postedBill.getLineItems().get(0); + assertNotNull(lineItem); + + // Should throw exception + assertThrows(IllegalStateException.class, () -> billLineItemService.purge(lineItem)); + } +} diff --git a/api/src/test/java/org/openmrs/module/billing/impl/BillServiceImplTest.java b/api/src/test/java/org/openmrs/module/billing/impl/BillServiceImplTest.java index e8e382c1..49d5f974 100644 --- a/api/src/test/java/org/openmrs/module/billing/impl/BillServiceImplTest.java +++ b/api/src/test/java/org/openmrs/module/billing/impl/BillServiceImplTest.java @@ -1,121 +1,361 @@ -///* -// * The contents of this file are subject to the OpenMRS Public License -// * Version 1.1 (the "License"); you may not use this file except in -// * compliance with the License. You may obtain a copy of the License at -// * http://license.openmrs.org -// * -// * Software distributed under the License is distributed on an "AS IS" -// * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the -// * License for the specific language governing rights and limitations -// * under the License. -// * -// * Copyright (C) OpenMRS, LLC. All Rights Reserved. -// */ -//package org.openmrs.module.cashier.api.impl; -// -//import static org.mockito.Mockito.times; -//import static org.mockito.Mockito.verify; -//import static org.powermock.api.mockito.PowerMockito.mock; -//import static org.powermock.api.mockito.PowerMockito.mockStatic; -//import static org.powermock.api.mockito.PowerMockito.when; -// -//import org.junit.*; -//import org.junit.Before; -//import org.junit.BeforeClass; -//import org.junit.Rule; -//import org.junit.Test; -//import org.openmrs.api.APIException; -//import org.openmrs.api.context.Context; -//import org.openmrs.module.cashier.api.IBillService; -//import org.openmrs.module.cashier.api.IBillServiceTest; -//import org.openmrs.module.cashier.api.IReceiptNumberGenerator; -//import org.openmrs.module.cashier.api.ReceiptNumberGeneratorFactory; -//import org.openmrs.module.cashier.api.model.Bill; -//import org.powermock.core.classloader.annotations.PrepareForTest; -//import org.powermock.modules.agent.PowerMockAgent; -//import org.powermock.modules.junit4.rule.PowerMockRule; -// -//@PrepareForTest(ReceiptNumberGeneratorFactory.class) -//public class BillServiceImplTest extends IBillServiceTest { -// @Rule -// public PowerMockRule rule = new PowerMockRule(); -// -// @BeforeClass -// public static void beforeClass() throws Exception { -// PowerMockAgent.initializeIfNeeded(); -// } -// -// IReceiptNumberGenerator receiptNumberGenerator; -// -// @Before -// public void before() throws Exception { -// super.before(); -// -// mockStatic(ReceiptNumberGeneratorFactory.class); -// receiptNumberGenerator = mock(IReceiptNumberGenerator.class); -// -// when(ReceiptNumberGeneratorFactory.getGenerator()) -// .thenReturn(receiptNumberGenerator); -// } -// -// @Override -// protected IBillService createService() { -// return Context.getService(IBillService.class); -// } -// -// /** -// * @verifies Generate a new receipt number if one has not been defined. -// * @see BillServiceImpl#save(Bill) -// */ -// @Test -// public void save_shouldGenerateANewReceiptNumberIfOneHasNotBeenDefined() throws Exception { -// Bill bill = createEntity(true); -// bill.setReceiptNumber(null); -// -// String receiptNumber = "Test Number"; -// when(receiptNumberGenerator.generateNumber(bill)) -// .thenReturn(receiptNumber); -// -// service.save(bill); -// Context.flushSession(); -// -// Bill savedBill = service.getById(bill.getId()); -// Assert.assertEquals(receiptNumber, savedBill.getReceiptNumber()); -// -// verify(receiptNumberGenerator, times(1)).generateNumber(bill); -// } -// -// /** -// * @verifies Not generate a receipt number if one has already been defined. -// * @see BillServiceImpl#save(Bill) -// */ -// @Test -// public void save_shouldNotGenerateAReceiptNumberIfOneHasAlreadyBeenDefined() throws Exception { -// String receiptNumber = "Test Number"; -// Bill bill = createEntity(true); -// bill.setReceiptNumber(receiptNumber); -// -// service.save(bill); -// Context.flushSession(); -// -// Bill savedBill = service.getById(bill.getId()); -// Assert.assertEquals(receiptNumber, savedBill.getReceiptNumber()); -// -// verify(receiptNumberGenerator, times(0)).generateNumber(bill); -// } -// -// /** -// * @verifies Throw APIException if receipt number cannot be generated. -// * @see BillServiceImpl#save(Bill) -// */ -// @Test(expected = APIException.class) -// public void save_shouldThrowAPIExceptionIfReceiptNumberCannotBeGenerated() throws Exception { -// Bill bill = createEntity(true); -// bill.setReceiptNumber(null); -// -// when(receiptNumberGenerator.generateNumber(bill)) -// .thenThrow(new APIException("Test exception")); -// -// service.save(bill); -// } -//} +/* + * The contents of this file are subject to the OpenMRS Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://license.openmrs.org + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the + * License for the specific language governing rights and limitations + * under the License. + * + * Copyright (C) OpenMRS, LLC. All Rights Reserved. + */ + + +package org.openmrs.module.billing.impl; + +import static org.junit.jupiter.api.Assertions.*; + +import java.math.BigDecimal; +import java.util.List; +import java.util.UUID; + +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openmrs.Patient; +import org.openmrs.api.PatientService; +import org.openmrs.api.ProviderService; +import org.openmrs.api.context.Context; +import org.openmrs.module.billing.TestConstants; +import org.openmrs.module.billing.api.IBillService; +import org.openmrs.module.billing.api.ICashPointService; +import org.openmrs.module.billing.api.model.Bill; +import org.openmrs.module.billing.api.model.BillLineItem; +import org.openmrs.module.billing.api.model.BillStatus; +import org.openmrs.module.stockmanagement.api.model.StockItem; +import org.openmrs.test.jupiter.BaseModuleContextSensitiveTest; + +public class BillServiceImplTest extends BaseModuleContextSensitiveTest { + + private IBillService billService; + + private ProviderService providerService; + + private PatientService patientService; + + private ICashPointService cashPointService; + + @BeforeEach + public void setup() { + billService = Context.getService(IBillService.class); + providerService = Context.getProviderService(); + patientService = Context.getPatientService(); + cashPointService = Context.getService(ICashPointService.class); + + executeDataSet(TestConstants.CORE_DATASET2); + executeDataSet(TestConstants.BASE_DATASET_DIR + "StockOperationType.xml"); + executeDataSet(TestConstants.BASE_DATASET_DIR + "PaymentModeTest.xml"); + executeDataSet(TestConstants.BASE_DATASET_DIR + "CashPointTest.xml"); + executeDataSet(TestConstants.BASE_DATASET_DIR + "BillTest.xml"); + } + + /** + * @see org.openmrs.module.billing.api.impl.BillServiceImpl#save(Bill) + */ + @Test + public void save_shouldThrowNullPointerExceptionIfBillIsNull() { + assertThrows(NullPointerException.class, () -> billService.save(null)); + } + + /** + * @see org.openmrs.module.billing.api.impl.BillServiceImpl#getBillByReceiptNumber(String) + */ + @Test + public void getBillByReceiptNumber_shouldThrowIllegalArgumentExceptionIfReceiptNumberIsNull() { + assertThrows(IllegalArgumentException.class, () -> billService.getBillByReceiptNumber(null)); + } + + /** + * @see org.openmrs.module.billing.api.impl.BillServiceImpl#getBillByReceiptNumber(String) + */ + @Test + public void getBillByReceiptNumber_shouldThrowIllegalArgumentExceptionIfReceiptNumberIsEmpty() { + assertThrows(IllegalArgumentException.class, () -> billService.getBillByReceiptNumber("")); + } + + /** + * @see org.openmrs.module.billing.api.impl.BillServiceImpl#getBillByReceiptNumber(String) + */ + @Test + public void getBillByReceiptNumber_shouldThrowIllegalArgumentExceptionIfReceiptNumberIsTooLong() { + String longReceiptNumber = RandomStringUtils.randomAlphanumeric(1999); + assertThrows(IllegalArgumentException.class, () -> billService.getBillByReceiptNumber(longReceiptNumber)); + } + + /** + * @see org.openmrs.module.billing.api.impl.BillServiceImpl#getBillsByPatient(Patient, + * org.openmrs.module.billing.api.base.PagingInfo) + */ + @Test + public void getBillsByPatient_shouldThrowNullPointerExceptionIfPatientIsNull() { + assertThrows(NullPointerException.class, () -> billService.getBillsByPatient(null, null)); + } + + /** + * @see org.openmrs.module.billing.api.impl.BillServiceImpl#getBillsByPatientId(int, + * org.openmrs.module.billing.api.base.PagingInfo) + */ + @Test + public void getBillsByPatientId_shouldThrowIllegalArgumentExceptionIfPatientIdIsNegative() { + assertThrows(IllegalArgumentException.class, () -> billService.getBillsByPatientId(-1, null)); + } + + /** + * @see org.openmrs.module.billing.api.impl.BillServiceImpl#getAll() + */ + @Test + public void getAll_shouldReturnAllBills() { + List bills = billService.getAll(); + assertNotNull(bills); + for (Bill bill : bills) { + if (bill.getLineItems() != null) { + for (Object item : bill.getLineItems()) { + assertNotNull(item, "Line items should not contain null values"); + } + } + } + } + + /** + * @see org.openmrs.module.billing.api.impl.BillServiceImpl#getBillByReceiptNumber(String) + */ + @Test + public void getBillByReceiptNumber_shouldReturnBillWithSpecifiedReceiptNumber() { + Bill bill = billService.getBillByReceiptNumber("test 1 receipt number"); + assertNotNull(bill); + assertEquals("test 1 receipt number", bill.getReceiptNumber()); + } + + /** + * @see org.openmrs.module.billing.api.impl.BillServiceImpl#getBillByReceiptNumber(String) + */ + @Test + public void getBillByReceiptNumber_shouldReturnNullIfReceiptNumberNotFound() { + Bill bill = billService.getBillByReceiptNumber("nonexistent receipt number"); + assertNull(bill); + } + + /** + * @see org.openmrs.module.billing.api.impl.BillServiceImpl#getBillsByPatientId(int, + * org.openmrs.module.billing.api.base.PagingInfo) + */ + @Test + public void getBillsByPatientId_shouldReturnBillsForPatient() { + List bills = billService.getBillsByPatientId(0, null); + assertNotNull(bills); + assertFalse(bills.isEmpty()); + assertEquals(1, bills.size()); + } + + /** + * @see org.openmrs.module.billing.api.impl.BillServiceImpl#getBillsByPatientId(int, + * org.openmrs.module.billing.api.base.PagingInfo) + */ + @Test + public void getBillsByPatientId_shouldReturnEmptyListWhenPatientHasNoBills() { + List bills = billService.getBillsByPatientId(999, null); + assertNotNull(bills); + assertEquals(0, bills.size()); + } + + /** + * @see org.openmrs.module.billing.api.impl.BillServiceImpl#save(Bill) + */ + @Test + public void save_shouldCreateNewBillWithNewItem() { + Patient patient = patientService.getPatient(1); + assertNotNull(patient); + + Bill templateBill = billService.getById(0); + assertNotNull(templateBill); + assertFalse(templateBill.getLineItems().isEmpty()); + + Bill newBill = new Bill(); + newBill.setCashier(providerService.getProvider(0)); + newBill.setPatient(patient); + newBill.setCashPoint(cashPointService.getById(0)); + newBill.setReceiptNumber("TEST-" + UUID.randomUUID()); + newBill.setStatus(BillStatus.PENDING); + + BillLineItem existingItem = templateBill.getLineItems().get(0); + StockItem stockItem = existingItem.getItem(); + + BillLineItem lineItem = newBill.addLineItem(stockItem, BigDecimal.valueOf(150), "New price", 2); + lineItem.setPaymentStatus(BillStatus.PENDING); + lineItem.setUuid(UUID.randomUUID().toString()); + + Bill savedBill = billService.save(newBill); + Context.flushSession(); + + assertNotNull(savedBill); + assertNotNull(savedBill.getId()); + assertEquals(BillStatus.PENDING, savedBill.getStatus()); + assertEquals(1, savedBill.getLineItems().size()); + assertEquals(BigDecimal.valueOf(300), savedBill.getTotal()); + + Bill retrievedBill = billService.getById(savedBill.getId()); + assertNotNull(retrievedBill); + assertEquals(patient.getId(), retrievedBill.getPatient().getId()); + } + + /** + * @see org.openmrs.module.billing.api.impl.BillServiceImpl#save(Bill) + */ + @Test + public void save_shouldUpdateExistingBillWithUpdatedBillItem() { + Bill pendingBill = billService.getById(2); + assertNotNull(pendingBill); + assertEquals(BillStatus.PENDING, pendingBill.getStatus()); + assertFalse(pendingBill.getLineItems().isEmpty()); + + BillLineItem firstItem = pendingBill.getLineItems().get(0); + BigDecimal updatedPrice = firstItem.getPrice().add(BigDecimal.TEN); + firstItem.setPrice(updatedPrice); + + billService.save(pendingBill); + Context.flushSession(); + Context.clearSession(); + + Bill updatedBill = billService.getById(2); + + assertEquals(pendingBill, updatedBill); + assertEquals(updatedPrice, updatedBill.getLineItems().get(0).getPrice()); + } + + /** + * @see org.openmrs.module.billing.api.impl.BillServiceImpl#getById(int) + */ + @Test + public void getById_shouldReturnBillWithSpecifiedId() { + Bill bill = billService.getById(1); + assertNotNull(bill); + assertEquals(1, bill.getId()); + } + + /** + * @see org.openmrs.module.billing.api.impl.BillServiceImpl#getById(int) + */ + @Test + public void getById_shouldRemoveNullLineItems() { + Bill bill = billService.getById(1); + assertNotNull(bill); + if (bill.getLineItems() != null) { + for (Object item : bill.getLineItems()) { + assertNotNull(item, "Line items should not contain null values"); + } + } + } + + /** + * @see org.openmrs.module.billing.api.impl.BillServiceImpl#save(Bill) + */ + @Test + public void save_shouldAllowAddingLineItemsToPendingBill() { + // Get the PENDING bill from test data (bill_id=2) + Bill pendingBill = billService.getById(2); + assertNotNull(pendingBill); + assertEquals(BillStatus.PENDING, pendingBill.getStatus()); + + // Add a new line item + BillLineItem newLineItem = new BillLineItem(); + newLineItem.setPrice(BigDecimal.valueOf(25.50)); + newLineItem.setQuantity(2); + newLineItem.setPaymentStatus(BillStatus.PENDING); + newLineItem.setLineItemOrder(pendingBill.getLineItems().size()); + pendingBill.addLineItem(newLineItem); + + // Should not throw exception + Bill savedBill = billService.save(pendingBill); + assertNotNull(savedBill); + assertTrue(savedBill.getLineItems().size() > 0); + } + + /** + * @see org.openmrs.module.billing.api.impl.BillServiceImpl#save(Bill) + */ + @Test + public void save_shouldThrowExceptionWhenAddingLineItemsToPostedBill() { + // Get the POSTED bill from test data (bill_id=0) + Bill postedBill = billService.getById(0); + assertNotNull(postedBill); + assertEquals(BillStatus.POSTED, postedBill.getStatus()); + + // Try to add a new line item + BillLineItem newLineItem = new BillLineItem(); + newLineItem.setPrice(BigDecimal.valueOf(25.50)); + newLineItem.setQuantity(2); + + // Should throw exception + assertThrows(IllegalStateException.class, () -> postedBill.addLineItem(newLineItem)); + } + + /** + * @see org.openmrs.module.billing.api.impl.BillServiceImpl#save(Bill) + */ + @Test + public void save_shouldThrowExceptionWhenAddingLineItemsToPaidBill() { + // Get the PAID bill from test data (bill_id=1) + Bill paidBill = billService.getById(1); + assertNotNull(paidBill); + assertEquals(BillStatus.PAID, paidBill.getStatus()); + + // Try to add a new line item + BillLineItem newLineItem = new BillLineItem(); + newLineItem.setPrice(BigDecimal.valueOf(25.50)); + newLineItem.setQuantity(2); + + // Should throw exception + assertThrows(IllegalStateException.class, () -> paidBill.addLineItem(newLineItem)); + } + + /** + * @see org.openmrs.module.billing.api.impl.BillServiceImpl#save(Bill) + */ + @Test + public void save_shouldAllowRemovingLineItemsFromPendingBill() { + // Get the PENDING bill from test data (bill_id=2) + Bill pendingBill = billService.getById(2); + assertNotNull(pendingBill); + assertEquals(BillStatus.PENDING, pendingBill.getStatus()); + + int originalSize = pendingBill.getLineItems().size(); + assertTrue(originalSize > 0); + + // Remove a line item + BillLineItem itemToRemove = pendingBill.getLineItems().get(0); + pendingBill.removeLineItem(itemToRemove); + + // Should not throw exception + Bill savedBill = billService.save(pendingBill); + assertNotNull(savedBill); + assertTrue(savedBill.getLineItems().size() < originalSize); + } + + /** + * @see org.openmrs.module.billing.api.impl.BillServiceImpl#save(Bill) + */ + @Test + public void save_shouldThrowExceptionWhenRemovingLineItemsFromPostedBill() { + // Get the POSTED bill from test data (bill_id=0) + Bill postedBill = billService.getById(0); + assertNotNull(postedBill); + assertEquals(BillStatus.POSTED, postedBill.getStatus()); + + BillLineItem itemToRemove = postedBill.getLineItems().get(0); + + // Should throw exception + assertThrows(IllegalStateException.class, () -> postedBill.removeLineItem(itemToRemove)); + } +} diff --git a/api/src/test/java/org/openmrs/module/billing/impl/CashPointServiceImplTest.java b/api/src/test/java/org/openmrs/module/billing/impl/CashPointServiceImplTest.java new file mode 100644 index 00000000..061cbc0f --- /dev/null +++ b/api/src/test/java/org/openmrs/module/billing/impl/CashPointServiceImplTest.java @@ -0,0 +1,190 @@ +/* + * The contents of this file are subject to the OpenMRS Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://license.openmrs.org + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the + * License for the specific language governing rights and limitations + * under the License. + * + * Copyright (C) OpenMRS, LLC. All Rights Reserved. + */ + +package org.openmrs.module.billing.impl; + +import java.util.List; + +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openmrs.Location; +import org.openmrs.api.LocationService; +import org.openmrs.api.context.Context; +import org.openmrs.module.billing.TestConstants; +import org.openmrs.module.billing.api.ICashPointService; +import org.openmrs.module.billing.api.model.CashPoint; +import org.openmrs.test.jupiter.BaseModuleContextSensitiveTest; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class CashPointServiceImplTest extends BaseModuleContextSensitiveTest { + + private ICashPointService cashPointService; + + private LocationService locationService; + + @BeforeEach + public void setup() { + cashPointService = Context.getService(ICashPointService.class); + locationService = Context.getLocationService(); + executeDataSet(TestConstants.CORE_DATASET2); + executeDataSet(TestConstants.BASE_DATASET_DIR + "CashPointTest.xml"); + } + + /** + * @see org.openmrs.module.billing.api.impl.CashPointServiceImpl#getCashPointsByLocation(Location, + * boolean) + */ + @Test + public void getCashPointsByLocation_shouldThrowIllegalArgumentExceptionIfLocationIsNull() { + assertThrows(IllegalArgumentException.class, () -> cashPointService.getCashPointsByLocation(null, false)); + } + + /** + * @see org.openmrs.module.billing.api.impl.CashPointServiceImpl#getCashPointsByLocation(Location, + * boolean) + */ + @Test + public void getCashPointsByLocation_shouldReturnCashPointsForLocation() { + Location location = locationService.getLocation(0); + assertNotNull(location); + List cashPoints = cashPointService.getCashPointsByLocation(location, false); + assertNotNull(cashPoints); + assertFalse(cashPoints.isEmpty()); + for (CashPoint cashPoint : cashPoints) { + assertEquals(location.getId(), cashPoint.getLocation().getId()); + } + } + + /** + * @see org.openmrs.module.billing.api.impl.CashPointServiceImpl#getCashPointsByLocation(Location, + * boolean) + */ + @Test + public void getCashPointsByLocation_shouldReturnEmptyListWhenLocationHasNoCashPoints() { + Location location = locationService.getLocation(999); + assertNotNull(location); + List cashPoints = cashPointService.getCashPointsByLocation(location, false); + assertNotNull(cashPoints); + assertTrue(cashPoints.isEmpty()); + } + + /** + * @see org.openmrs.module.billing.api.impl.CashPointServiceImpl#getCashPointsByLocationAndName(Location, + * String, boolean) + */ + @Test + public void getCashPointsByLocationAndName_shouldThrowIllegalArgumentExceptionIfLocationIsNull() { + assertThrows(IllegalArgumentException.class, + () -> cashPointService.getCashPointsByLocationAndName(null, "Test", false)); + } + + /** + * @see org.openmrs.module.billing.api.impl.CashPointServiceImpl#getCashPointsByLocationAndName(Location, + * String, boolean) + */ + @Test + public void getCashPointsByLocationAndName_shouldThrowIllegalArgumentExceptionIfNameIsNull() { + Location location = locationService.getLocation(0); + assertThrows(IllegalArgumentException.class, + () -> cashPointService.getCashPointsByLocationAndName(location, null, false)); + } + + /** + * @see org.openmrs.module.billing.api.impl.CashPointServiceImpl#getCashPointsByLocationAndName(Location, + * String, boolean) + */ + @Test + public void getCashPointsByLocationAndName_shouldThrowIllegalArgumentExceptionIfNameIsEmpty() { + Location location = locationService.getLocation(0); + assertThrows(IllegalArgumentException.class, + () -> cashPointService.getCashPointsByLocationAndName(location, "", false)); + } + + /** + * @see org.openmrs.module.billing.api.impl.CashPointServiceImpl#getCashPointsByLocationAndName(Location, + * String, boolean) + */ + @Test + public void getCashPointsByLocationAndName_shouldThrowIllegalArgumentExceptionIfNameIsTooLong() { + Location location = locationService.getLocation(0); + String longName = RandomStringUtils.randomAlphanumeric(256); + assertThrows(IllegalArgumentException.class, + () -> cashPointService.getCashPointsByLocationAndName(location, longName, false)); + } + + /** + * @see org.openmrs.module.billing.api.impl.CashPointServiceImpl#getCashPointsByLocationAndName(Location, + * String, boolean) + */ + @Test + public void getCashPointsByLocationAndName_shouldReturnCashPointsMatchingLocationAndName() { + Location location = locationService.getLocation(0); + List cashPoints = cashPointService.getCashPointsByLocationAndName(location, "Test", false); + assertNotNull(cashPoints); + assertFalse(cashPoints.isEmpty()); + for (CashPoint cashPoint : cashPoints) { + assertEquals(location.getId(), cashPoint.getLocation().getId()); + assertTrue(cashPoint.getName().startsWith("Test")); + } + } + + /** + * @see org.openmrs.module.billing.api.impl.CashPointServiceImpl#getCashPointsByLocationAndName(Location, + * String, boolean) + */ + @Test + public void getCashPointsByLocationAndName_shouldReturnEmptyListWhenNoMatch() { + Location location = locationService.getLocation(0); + List cashPoints = cashPointService.getCashPointsByLocationAndName(location, "Fake name", false); + assertNotNull(cashPoints); + assertTrue(cashPoints.isEmpty()); + } + + /** + * @see org.openmrs.module.billing.api.impl.CashPointServiceImpl#getById(int) + */ + @Test + public void getById_shouldReturnCashPointWithSpecifiedId() { + CashPoint cashPoint = cashPointService.getById(0); + assertNotNull(cashPoint); + assertEquals(0, cashPoint.getId()); + } + + /** + * @see org.openmrs.module.billing.api.impl.CashPointServiceImpl#getByUuid(String) + */ + @Test + public void getByUuid_shouldReturnCashPointWithSpecifiedUuid() { + CashPoint cashPoint = cashPointService.getByUuid("4028814B39BB04B90139BB04B98B0000"); + assertNotNull(cashPoint); + assertEquals("4028814B39BB04B90139BB04B98B0000", cashPoint.getUuid()); + } + + /** + * @see org.openmrs.module.billing.api.impl.CashPointServiceImpl#getAll() + */ + @Test + public void getAll_shouldReturnAllCashPoints() { + List cashPoints = cashPointService.getAll(); + assertNotNull(cashPoints); + assertFalse(cashPoints.isEmpty()); + assertEquals(7, cashPoints.size()); + } +} diff --git a/api/src/test/java/org/openmrs/module/billing/impl/CashierOptionsServiceGpImplTest.java b/api/src/test/java/org/openmrs/module/billing/impl/CashierOptionsServiceGpImplTest.java index 53517792..4501ecd3 100644 --- a/api/src/test/java/org/openmrs/module/billing/impl/CashierOptionsServiceGpImplTest.java +++ b/api/src/test/java/org/openmrs/module/billing/impl/CashierOptionsServiceGpImplTest.java @@ -1,198 +1,190 @@ -///* -// * The contents of this file are subject to the OpenMRS Public License -// * Version 1.1 (the "License"); you may not use this file except in -// * compliance with the License. You may obtain a copy of the License at -// * http://license.openmrs.org -// * -// * Software distributed under the License is distributed on an "AS IS" -// * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the -// * License for the specific language governing rights and limitations -// * under the License. -// * -// * Copyright (C) OpenMRS, LLC. All Rights Reserved. -// */ -//package org.openmrs.module.cashier.api.impl; -// -//import static org.junit.Assert.assertFalse; -//import static org.junit.Assert.assertNotNull; -//import static org.powermock.api.mockito.PowerMockito.mock; -//import static org.powermock.api.mockito.PowerMockito.when; -// -//import java.io.ByteArrayOutputStream; -// -//import org.apache.log4j.Appender; -//import org.apache.log4j.Layout; -//import org.apache.log4j.Logger; -//import org.apache.log4j.SimpleLayout; -//import org.apache.log4j.WriterAppender; -//import org.junit.Assert; -//import org.junit.Before; -//import org.junit.Test; -//import org.junit.runner.RunWith; -//import org.openmrs.api.AdministrationService; -//import org.openmrs.module.cashier.ModuleSettings; -//import org.openmrs.module.cashier.api.impl.CashierOptionsServiceGpImpl; -//import org.openmrs.module.cashier.api.model.CashierOptions; -//import org.openmrs.module.openhmis.inventory.api.IItemDataService; -//import org.openmrs.module.openhmis.inventory.api.model.Item; -//import org.powermock.modules.junit4.PowerMockRunner; -// -//@RunWith(PowerMockRunner.class) -//public class CashierOptionsServiceGpImplTest { -// private CashierOptionsServiceGpImpl optionsService = null; -// private AdministrationService adminService = null; -// private IItemDataService itemService = null; -// -// @Before -// public void before() { -// adminService = mock(AdministrationService.class); -// itemService = mock(IItemDataService.class); -// -// optionsService = new CashierOptionsServiceGpImpl(); -// } -// -// /** -// * @verifies load cashier options from the database -// * @see CashierOptionsServiceGpImpl#getOptions() -// */ -// @Test -// public void getOptions_shouldLoadCashierOptionsFromTheDatabase() throws Exception { -// when(adminService.getGlobalProperty(ModuleSettings.RECEIPT_REPORT_ID_PROPERTY)) -// .thenReturn("1"); -// when(adminService.getGlobalProperty(ModuleSettings.ROUNDING_MODE_PROPERTY)) -// .thenReturn(CashierOptions.RoundingMode.MID.toString()); -// when(adminService.getGlobalProperty(ModuleSettings.ROUND_TO_NEAREST_PROPERTY)) -// .thenReturn("5"); -// when(adminService.getGlobalProperty(ModuleSettings.ROUNDING_ITEM_ID)) -// .thenReturn("1"); -// when(adminService.getGlobalProperty(ModuleSettings.TIMESHEET_REQUIRED_PROPERTY)) -// .thenReturn("true"); -// -// Item item = new Item(); -// when(itemService.getById(1)) -// .thenReturn(item); -// -// CashierOptions options = optionsService.getOptions(); -// -// Assert.assertNotNull(options); -// Assert.assertEquals(1, options.getDefaultReceiptReportId()); -// Assert.assertEquals(CashierOptions.RoundingMode.MID, options.getRoundingMode()); -// Assert.assertEquals(5, (int)options.getRoundToNearest()); -// Assert.assertEquals(item.getUuid(), options.getRoundingItemUuid()); -// Assert.assertEquals(true, options.isTimesheetRequired()); -// } -// -// /** -// * @verifies not throw exception if numeric options are null -// * @see CashierOptionsServiceGpImpl#getOptions() -// */ -// @Test -// public void getOptions_shouldNotThrowExceptionIfNumericOptionsAreNull() throws Exception { -// when(adminService.getGlobalProperty(ModuleSettings.RECEIPT_REPORT_ID_PROPERTY)) -// .thenReturn(null); -// when(adminService.getGlobalProperty(ModuleSettings.ROUNDING_MODE_PROPERTY)) -// .thenReturn(null); -// when(adminService.getGlobalProperty(ModuleSettings.ROUND_TO_NEAREST_PROPERTY)) -// .thenReturn(null); -// when(adminService.getGlobalProperty(ModuleSettings.ROUNDING_ITEM_ID)) -// .thenReturn(null); -// when(adminService.getGlobalProperty(ModuleSettings.TIMESHEET_REQUIRED_PROPERTY)) -// .thenReturn(null); -// -// CashierOptions options = optionsService.getOptions(); -// -// Assert.assertNotNull(options); -// } -// -// /** -// * @verifies default to false if timesheet required is not specified -// * @see CashierOptionsServiceGpImpl#getOptions() -// */ -// @Test -// public void getOptions_shouldDefaultToFalseIfTimesheetRequiredIsNotSpecified() throws Exception { -// when(adminService.getGlobalProperty(ModuleSettings.RECEIPT_REPORT_ID_PROPERTY)) -// .thenReturn(null); -// when(adminService.getGlobalProperty(ModuleSettings.ROUNDING_MODE_PROPERTY)) -// .thenReturn(null); -// when(adminService.getGlobalProperty(ModuleSettings.ROUND_TO_NEAREST_PROPERTY)) -// .thenReturn(null); -// when(adminService.getGlobalProperty(ModuleSettings.ROUNDING_ITEM_ID)) -// .thenReturn(null); -// when(adminService.getGlobalProperty(ModuleSettings.TIMESHEET_REQUIRED_PROPERTY)) -// .thenReturn(null); -// -// CashierOptions options = optionsService.getOptions(); -// -// Assert.assertNotNull(options); -// Assert.assertEquals(false, options.isTimesheetRequired()); -// } -// -// /** -// * @verifies log Error if Exception due to non-parsable rounding item id -// * @see CashierOptionsServiceGpImpl#getOptions() -// */ -// @Test -// public void getOptions_shouldLogErrorIfRoundingItemIdCannotBeParsed() throws Exception { -// -// when(adminService.getGlobalProperty(ModuleSettings.RECEIPT_REPORT_ID_PROPERTY)) -// .thenReturn(null); -// when(adminService.getGlobalProperty(ModuleSettings.ROUNDING_MODE_PROPERTY)) -// .thenReturn(CashierOptions.RoundingMode.FLOOR.toString()); -// when(adminService.getGlobalProperty(ModuleSettings.ROUND_TO_NEAREST_PROPERTY)) -// .thenReturn("5"); -// when(adminService.getGlobalProperty(ModuleSettings.ROUNDING_ITEM_ID)) -// .thenReturn("HELP"); -// -// Logger logger = Logger.getLogger(CashierOptionsServiceGpImpl.class); -// -// ByteArrayOutputStream out = new ByteArrayOutputStream(); -// Layout layout = new SimpleLayout(); -// Appender appender = new WriterAppender(layout, out); -// logger.addAppender(appender); -// -// try { -// optionsService.getOptions(); -// String logMsg = out.toString(); -// assertNotNull(logMsg); -// assertFalse((logMsg.trim()).equals("")); -// } finally { -// logger.removeAppender(appender); -// } -// } -// -// /** -// * @verifies log error if rouding item id is set but item cannot be found (and hence is null) -// * @see CashierOptionsServiceGpImpl#getOptions() -// */ -// @Test -// public void getOptions_shouldLogErrorIfRoundingItemIsNullDespiteIdGiven() throws Exception { -// -// when(adminService.getGlobalProperty(ModuleSettings.RECEIPT_REPORT_ID_PROPERTY)) -// .thenReturn(null); -// when(adminService.getGlobalProperty(ModuleSettings.ROUNDING_MODE_PROPERTY)) -// .thenReturn(CashierOptions.RoundingMode.FLOOR.toString()); -// when(adminService.getGlobalProperty(ModuleSettings.ROUND_TO_NEAREST_PROPERTY)) -// .thenReturn("5"); -// when(adminService.getGlobalProperty(ModuleSettings.ROUNDING_ITEM_ID)) -// .thenReturn("273423"); -// when(adminService.getGlobalProperty(ModuleSettings.TIMESHEET_REQUIRED_PROPERTY)) -// .thenReturn(null); -// -// Logger logger = Logger.getLogger(CashierOptionsServiceGpImpl.class); -// -// ByteArrayOutputStream out = new ByteArrayOutputStream(); -// Layout layout = new SimpleLayout(); -// Appender appender = new WriterAppender(layout, out); -// logger.addAppender(appender); -// -// try { -// optionsService.getOptions(); -// String logMsg = out.toString(); -// assertNotNull(logMsg); -// assertFalse((logMsg.trim()).equals("")); -// } finally { -// logger.removeAppender(appender); -// } -// } -// -//} +/* + * The contents of this file are subject to the OpenMRS Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://license.openmrs.org + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the + * License for the specific language governing rights and limitations + * under the License. + * + * Copyright (C) OpenMRS, LLC. All Rights Reserved. + */ + +package org.openmrs.module.billing.impl; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openmrs.api.AdministrationService; +import org.openmrs.api.context.Context; +import org.openmrs.module.billing.ModuleSettings; +import org.openmrs.module.billing.api.impl.CashierOptionsServiceGpImpl; +import org.openmrs.module.billing.api.model.CashierOptions; +import org.openmrs.test.jupiter.BaseModuleContextSensitiveTest; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class CashierOptionsServiceGpImplTest extends BaseModuleContextSensitiveTest { + + private CashierOptionsServiceGpImpl service; + + private AdministrationService adminService; + + @BeforeEach + public void setup() { + service = new CashierOptionsServiceGpImpl(); + adminService = Context.getAdministrationService(); + } + + /** + * @see org.openmrs.module.billing.api.impl.CashierOptionsServiceGpImpl#getOptions() + */ + @Test + public void getOptions_shouldReturnCashierOptionsWithDefaults() { + CashierOptions options = service.getOptions(); + assertNotNull(options); + assertFalse(options.isTimesheetRequired()); + assertEquals(CashierOptions.RoundingMode.MID, options.getRoundingMode()); + assertEquals(0, options.getRoundToNearest()); + } + + /** + * @see org.openmrs.module.billing.api.impl.CashierOptionsServiceGpImpl#getOptions() + */ + @Test + public void getOptions_shouldLoadDefaultReceiptReportIdFromGlobalProperty() { + adminService.setGlobalProperty(ModuleSettings.RECEIPT_REPORT_ID_PROPERTY, "123"); + + CashierOptions options = service.getOptions(); + assertNotNull(options); + assertEquals(123, options.getDefaultReceiptReportId()); + } + + /** + * @see org.openmrs.module.billing.api.impl.CashierOptionsServiceGpImpl#getOptions() + */ + @Test + public void getOptions_shouldHandleInvalidReceiptReportId() { + adminService.setGlobalProperty(ModuleSettings.RECEIPT_REPORT_ID_PROPERTY, "invalid"); + + CashierOptions options = service.getOptions(); + assertNotNull(options); + assertEquals(0, options.getDefaultReceiptReportId()); + } + + /** + * @see org.openmrs.module.billing.api.impl.CashierOptionsServiceGpImpl#getOptions() + */ + @Test + public void getOptions_shouldLoadTimesheetRequiredFromGlobalProperty() { + adminService.setGlobalProperty(ModuleSettings.TIMESHEET_REQUIRED_PROPERTY, "true"); + + CashierOptions options = service.getOptions(); + assertNotNull(options); + assertTrue(options.isTimesheetRequired()); + } + + /** + * @see org.openmrs.module.billing.api.impl.CashierOptionsServiceGpImpl#getOptions() + */ + @Test + public void getOptions_shouldDefaultToFalseIfTimesheetRequiredIsNotSpecified() { + adminService.setGlobalProperty(ModuleSettings.TIMESHEET_REQUIRED_PROPERTY, ""); + + CashierOptions options = service.getOptions(); + assertNotNull(options); + assertFalse(options.isTimesheetRequired()); + } + + /** + * @see org.openmrs.module.billing.api.impl.CashierOptionsServiceGpImpl#getOptions() + */ + @Test + public void getOptions_shouldHandleInvalidTimesheetRequiredValue() { + adminService.setGlobalProperty(ModuleSettings.TIMESHEET_REQUIRED_PROPERTY, "invalid"); + + CashierOptions options = service.getOptions(); + assertNotNull(options); + assertFalse(options.isTimesheetRequired()); + } + + /** + * @see org.openmrs.module.billing.api.impl.CashierOptionsServiceGpImpl#getOptions() + */ + @Test + public void getOptions_shouldSetDefaultRoundingOptionsWhenRoundingItemUuidIsEmpty() { + CashierOptions options = service.getOptions(); + assertNotNull(options); + assertEquals(CashierOptions.RoundingMode.MID, options.getRoundingMode()); + assertEquals(0, options.getRoundToNearest()); + } + + /** + * @see org.openmrs.module.billing.api.impl.CashierOptionsServiceGpImpl#getOptions() + */ + @Test + public void getOptions_shouldNotThrowExceptionIfNumericOptionsAreNull() { + adminService.setGlobalProperty(ModuleSettings.RECEIPT_REPORT_ID_PROPERTY, ""); + adminService.setGlobalProperty(ModuleSettings.ROUND_TO_NEAREST_PROPERTY, ""); + + assertDoesNotThrow(() -> { + CashierOptions options = service.getOptions(); + assertNotNull(options); + }); + } + + /** + * @see org.openmrs.module.billing.api.impl.CashierOptionsServiceGpImpl#getOptions() + */ + @Test + public void getOptions_shouldHandleMultiplePropertiesSet() { + adminService.setGlobalProperty(ModuleSettings.RECEIPT_REPORT_ID_PROPERTY, "456"); + adminService.setGlobalProperty(ModuleSettings.TIMESHEET_REQUIRED_PROPERTY, "true"); + + CashierOptions options = service.getOptions(); + assertNotNull(options); + assertEquals(456, options.getDefaultReceiptReportId()); + assertTrue(options.isTimesheetRequired()); + } + + /** + * @see org.openmrs.module.billing.api.impl.CashierOptionsServiceGpImpl#getOptions() + */ + @Test + public void getOptions_shouldLoadCashierOptionsFromTheDatabase() { + adminService.setGlobalProperty(ModuleSettings.RECEIPT_REPORT_ID_PROPERTY, "1"); + adminService.setGlobalProperty(ModuleSettings.TIMESHEET_REQUIRED_PROPERTY, "true"); + + CashierOptions options = service.getOptions(); + + assertNotNull(options); + assertEquals(1, options.getDefaultReceiptReportId()); + assertEquals(CashierOptions.RoundingMode.MID, options.getRoundingMode()); + assertEquals(0, options.getRoundToNearest()); + assertTrue(options.isTimesheetRequired()); + } + + /** + * @see org.openmrs.module.billing.api.impl.CashierOptionsServiceGpImpl#getOptions() + */ + @Test + public void getOptions_shouldHandleNullGlobalProperties() { + adminService.setGlobalProperty(ModuleSettings.RECEIPT_REPORT_ID_PROPERTY, null); + adminService.setGlobalProperty(ModuleSettings.ROUNDING_MODE_PROPERTY, null); + adminService.setGlobalProperty(ModuleSettings.ROUND_TO_NEAREST_PROPERTY, null); + adminService.setGlobalProperty(ModuleSettings.TIMESHEET_REQUIRED_PROPERTY, null); + + CashierOptions options = service.getOptions(); + + assertNotNull(options); + assertEquals(0, options.getDefaultReceiptReportId()); + assertEquals(CashierOptions.RoundingMode.MID, options.getRoundingMode()); + assertEquals(0, options.getRoundToNearest()); + assertFalse(options.isTimesheetRequired()); + } +} diff --git a/api/src/test/resources/org/openmrs/module/billing/api/include/BillTest.xml b/api/src/test/resources/org/openmrs/module/billing/api/include/BillTest.xml index a4d909aa..b2fb47b7 100644 --- a/api/src/test/resources/org/openmrs/module/billing/api/include/BillTest.xml +++ b/api/src/test/resources/org/openmrs/module/billing/api/include/BillTest.xml @@ -1,19 +1,45 @@ + + + + + + + + + + + + + + + + + + diff --git a/api/src/test/resources/org/openmrs/module/billing/api/include/CoreTest-2.0.xml b/api/src/test/resources/org/openmrs/module/billing/api/include/CoreTest-2.0.xml index 382abef9..88961198 100644 --- a/api/src/test/resources/org/openmrs/module/billing/api/include/CoreTest-2.0.xml +++ b/api/src/test/resources/org/openmrs/module/billing/api/include/CoreTest-2.0.xml @@ -27,6 +27,9 @@ uuid="ef93c695-ac43-450a-93f8-4b2b4d50a3c8"/> + + + + + + + + + + + + + + + diff --git a/fhir/src/test/java/org/openmrs/module/billing/impl/FhirInvoiceServiceImplTest.java b/fhir/src/test/java/org/openmrs/module/billing/impl/FhirInvoiceServiceImplTest.java index e9d0f1db..1bababb3 100644 --- a/fhir/src/test/java/org/openmrs/module/billing/impl/FhirInvoiceServiceImplTest.java +++ b/fhir/src/test/java/org/openmrs/module/billing/impl/FhirInvoiceServiceImplTest.java @@ -15,7 +15,6 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.notNullValue; -import static org.hamcrest.Matchers.nullValue; import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) diff --git a/omod/src/main/java/org/openmrs/module/billing/web/rest/controller/ReceiptController.java b/omod/src/main/java/org/openmrs/module/billing/web/rest/controller/ReceiptController.java index d61a4993..b6f5341a 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/rest/controller/ReceiptController.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/rest/controller/ReceiptController.java @@ -37,22 +37,25 @@ public class ReceiptController extends BaseRestController { @RequestMapping(method = RequestMethod.GET) - public ResponseEntity get(@RequestParam(value = "billId", required = false) Integer billId) throws IOException { + public ResponseEntity get(@RequestParam(value = "billUuid", required = false) String billUuid) + throws IOException { IBillService service = Context.getService(IBillService.class); - Bill bill = service.getById(billId); + Bill bill = service.getByUuid(billUuid); if (bill == null) { - return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + return new ResponseEntity<>(HttpStatus.NOT_FOUND); } byte[] pdfFile = service.downloadBillReceipt(bill); - if (pdfFile.length > 0) { + if (pdfFile != null && pdfFile.length > 0) { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_PDF); + headers.setContentLength(pdfFile.length); + headers.add(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"receipt-" + bill.getId() + ".pdf\""); return new ResponseEntity<>(pdfFile, headers, HttpStatus.OK); } else { - return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); } } diff --git a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillLineItemResource.java b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillLineItemResource.java index 4d357c63..42f4e73d 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillLineItemResource.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillLineItemResource.java @@ -59,8 +59,9 @@ public DelegatingResourceDescription getRepresentationDescription(Representation description.addProperty("priceUuid"); description.addProperty("lineItemOrder"); description.addProperty("paymentStatus"); + return description; } - return description; + return null; } @PropertySetter(value = "item") diff --git a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillResource.java b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillResource.java index c0b89f39..0a4146c9 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillResource.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillResource.java @@ -14,12 +14,14 @@ package org.openmrs.module.billing.web.rest.resource; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.stream.Collectors; -import org.apache.logging.log4j.util.Strings; +import org.apache.commons.lang3.StringUtils; import org.openmrs.Patient; import org.openmrs.Provider; import org.openmrs.User; @@ -30,6 +32,7 @@ import org.openmrs.module.billing.api.IBillService; import org.openmrs.module.billing.api.ICashPointService; import org.openmrs.module.billing.api.ITimesheetService; +import org.openmrs.module.billing.api.base.PagingInfo; import org.openmrs.module.billing.api.base.entity.IEntityDataService; import org.openmrs.module.billing.api.model.Bill; import org.openmrs.module.billing.api.model.BillLineItem; @@ -40,13 +43,14 @@ import org.openmrs.module.billing.api.search.BillSearch; import org.openmrs.module.billing.api.util.RoundingUtil; import org.openmrs.module.billing.web.base.resource.BaseRestDataResource; +import org.openmrs.module.billing.web.base.resource.PagingUtil; import org.openmrs.module.billing.web.rest.controller.base.CashierResourceController; import org.openmrs.module.webservices.rest.web.RequestContext; import org.openmrs.module.webservices.rest.web.RestConstants; import org.openmrs.module.webservices.rest.web.annotation.PropertySetter; import org.openmrs.module.webservices.rest.web.annotation.Resource; import org.openmrs.module.webservices.rest.web.representation.DefaultRepresentation; -import org.openmrs.module.webservices.rest.web.representation.RefRepresentation; +import org.openmrs.module.webservices.rest.web.representation.FullRepresentation; import org.openmrs.module.webservices.rest.web.representation.Representation; import org.openmrs.module.webservices.rest.web.resource.impl.AlreadyPaged; import org.openmrs.module.webservices.rest.web.resource.impl.DelegatingResourceDescription; @@ -61,7 +65,7 @@ public class BillResource extends BaseRestDataResource { @Override public DelegatingResourceDescription getRepresentationDescription(Representation rep) { DelegatingResourceDescription description = super.getRepresentationDescription(rep); - if (!(rep instanceof RefRepresentation)) { + if (rep instanceof DefaultRepresentation || rep instanceof FullRepresentation) { description.addProperty("adjustedBy", Representation.REF); description.addProperty("billAdjusted", Representation.REF); description.addProperty("cashPoint", Representation.REF); @@ -74,8 +78,9 @@ public DelegatingResourceDescription getRepresentationDescription(Representation description.addProperty("status"); description.addProperty("adjustmentReason"); description.addProperty("id"); + return description; } - return description; + return null; } @Override @@ -85,6 +90,11 @@ public DelegatingResourceDescription getCreatableProperties() { @PropertySetter("lineItems") public void setBillLineItems(Bill instance, List lineItems) { + if (!instance.isPending()) { + throw new IllegalStateException( + "Line items can only be modified when the bill is in PENDING state. Current status: " + + instance.getStatus()); + } if (instance.getLineItems() == null) { instance.setLineItems(new ArrayList(lineItems.size())); } @@ -165,10 +175,26 @@ protected AlreadyPaged doSearch(RequestContext context) { String patientUuid = context.getRequest().getParameter("patientUuid"); String status = context.getRequest().getParameter("status"); String cashPointUuid = context.getRequest().getParameter("cashPointUuid"); + String includeVoidedLineItemsParam = context.getRequest().getParameter("includeAll"); + String patientName = context.getRequest().getParameter("patientName"); - Patient patient = Strings.isNotEmpty(patientUuid) ? Context.getPatientService().getPatientByUuid(patientUuid) : null; - BillStatus billStatus = Strings.isNotEmpty(status) ? BillStatus.valueOf(status.toUpperCase()) : null; - CashPoint cashPoint = Strings.isNotEmpty(cashPointUuid) ? Context.getService(ICashPointService.class).getByUuid(cashPointUuid) : null; + Patient patient = StringUtils.isNotBlank(patientUuid) ? Context.getPatientService().getPatientByUuid(patientUuid) : null; + BillStatus billStatus = null; + List statusList = null; + if (StringUtils.isNotBlank(status)) { + // Support comma-separated statuses: status=PENDING,POSTED + String[] statusArray = status.split(","); + if (statusArray.length > 1) { + // Multiple statuses provided + statusList = Arrays.stream(statusArray) + .map(s -> BillStatus.valueOf(s.trim().toUpperCase())) + .collect(Collectors.toList()); + } else { + // Single status + billStatus = BillStatus.valueOf(status.trim().toUpperCase()); + } + } + CashPoint cashPoint = StringUtils.isNotBlank(cashPointUuid) ? Context.getService(ICashPointService.class).getByUuid(cashPointUuid) : null; Bill searchTemplate = new Bill(); searchTemplate.setPatient(patient); @@ -176,8 +202,42 @@ protected AlreadyPaged doSearch(RequestContext context) { searchTemplate.setCashPoint(cashPoint); IBillService service = Context.getService(IBillService.class); - List result = service.getBills(new BillSearch(searchTemplate, false)); - return new AlreadyPaged<>(context, result, false); + BillSearch billSearch = new BillSearch(searchTemplate, false); + + if (StringUtils.isNotBlank(patientName)) { + billSearch.setPatientName(patientName); + } + + // Set multiple statuses if provided, otherwise single status from template will be used + if (statusList != null && !statusList.isEmpty()) { + billSearch.setStatuses(statusList); + } + // Default to false (exclude voided line items) unless explicitly set to true + boolean includeVoidedLineItems = false; + if (StringUtils.isNotBlank(includeVoidedLineItemsParam)) { + includeVoidedLineItems = Boolean.parseBoolean(includeVoidedLineItemsParam); + } + billSearch.includeVoidedLineItems(includeVoidedLineItems); + PagingInfo pagingInfo = PagingUtil.getPagingInfoFromContext(context); + + List result = service.getBills(billSearch, pagingInfo); + return new AlreadyPaged<>(context, result, pagingInfo.hasMoreResults(), pagingInfo.getTotalRecordCount()); + } + + + /** + * Gets a bill by UUID + * + * @param uniqueId The bill UUID. + * @return The bill with the specified UUID without voided line items. + */ + @Override + public Bill getByUniqueId(String uniqueId) { + if (StringUtils.isBlank(uniqueId)) { + return null; + } + + return Context.getService(IBillService.class).getByUuid(uniqueId, false); } @SuppressWarnings("unchecked") diff --git a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillableServiceResource.java b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillableServiceResource.java index 7ae91672..4a77564d 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillableServiceResource.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillableServiceResource.java @@ -16,11 +16,14 @@ import org.apache.logging.log4j.util.Strings; import org.openmrs.Concept; import org.openmrs.api.context.Context; +import org.openmrs.module.billing.api.base.entity.IMetadataDataService; +import org.openmrs.module.billing.api.model.BillableService; +import org.openmrs.module.billing.api.model.BillableServiceStatus; +import org.openmrs.module.billing.api.model.CashierItemPrice; import org.openmrs.module.billing.web.base.resource.BaseRestDataResource; +import org.openmrs.module.billing.web.base.resource.BaseRestMetadataResource; import org.openmrs.module.billing.web.rest.controller.base.CashierResourceController; import org.openmrs.module.billing.api.IBillableItemsService; -import org.openmrs.module.billing.api.base.entity.IEntityDataService; -import org.openmrs.module.billing.api.model.*; import org.openmrs.module.billing.api.search.BillableServiceSearch; import org.openmrs.module.webservices.rest.web.RequestContext; import org.openmrs.module.webservices.rest.web.RestConstants; @@ -40,7 +43,7 @@ @Resource(name = RestConstants.VERSION_1 + CashierResourceController.BILLING_NAMESPACE + "/billableService", supportedClass = BillableService.class, supportedOpenmrsVersions = {"2.0 - 2.*"}) -public class BillableServiceResource extends BaseRestDataResource { +public class BillableServiceResource extends BaseRestMetadataResource { @Override public BillableService newDelegate() { @@ -48,7 +51,7 @@ public BillableService newDelegate() { } @Override - public Class> getServiceClass() { + public Class> getServiceClass() { return IBillableItemsService.class; } diff --git a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/CashierItemPriceResource.java b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/CashierItemPriceResource.java index e4943b51..a4e6e850 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/CashierItemPriceResource.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/CashierItemPriceResource.java @@ -14,10 +14,10 @@ package org.openmrs.module.billing.web.rest.resource; import org.openmrs.api.context.Context; -import org.openmrs.module.billing.web.base.resource.BaseRestDataResource; +import org.openmrs.module.billing.api.base.entity.IMetadataDataService; +import org.openmrs.module.billing.web.base.resource.BaseRestMetadataResource; import org.openmrs.module.billing.web.rest.controller.base.CashierResourceController; import org.openmrs.module.billing.api.ICashierItemPriceService; -import org.openmrs.module.billing.api.base.entity.IEntityDataService; import org.openmrs.module.billing.api.model.CashierItemPrice; import org.openmrs.module.stockmanagement.api.StockManagementService; import org.openmrs.module.stockmanagement.api.model.StockItem; @@ -36,14 +36,14 @@ @Resource(name = RestConstants.VERSION_1 + CashierResourceController.BILLING_NAMESPACE + "/cashierItemPrice", supportedClass = CashierItemPrice.class, supportedOpenmrsVersions = {"2.0 - 2.*"}) -public class CashierItemPriceResource extends BaseRestDataResource { +public class CashierItemPriceResource extends BaseRestMetadataResource { @Override public CashierItemPrice newDelegate() { return new CashierItemPrice(); } @Override - public Class> getServiceClass() { + public Class> getServiceClass() { return ICashierItemPriceService.class; } diff --git a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/PaymentResource.java b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/PaymentResource.java index b29958fd..58b9d12f 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/PaymentResource.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/PaymentResource.java @@ -48,18 +48,18 @@ public class PaymentResource extends DelegatingSubResource + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 9f552ecc..d815d5be 100644 --- a/pom.xml +++ b/pom.xml @@ -114,13 +114,13 @@ org.openmrs.module webservices.rest-omod - 2.9 + 2.49.0 provided org.openmrs.module webservices.rest-omod-common - 2.9 + 2.49.0 provided From 3a32ae9c9cdea87fbff6d4a863105abba94b25b4 Mon Sep 17 00:00:00 2001 From: Mutagubya Jonathan Date: Wed, 17 Dec 2025 14:48:33 +0300 Subject: [PATCH 10/20] Fixing failing tests (#11) --- .../module/billing/api/impl/BillServiceImpl.java | 2 +- .../module/billing/api/search/BillSearch.java | 2 +- .../module/billing/impl/BillServiceImplTest.java | 7 +++---- .../module/billing/api/include/BillTest.xml | 2 +- .../module/billing/api/include/CashPointTest.xml | 14 +++++++------- .../module/billing/api/include/PaymentModeTest.xml | 6 +++--- .../openmrs/module/billing/include/BillTest.xml | 2 +- .../module/billing/include/CashPointTest.xml | 14 +++++++------- .../module/billing/include/PaymentModeTest.xml | 6 +++--- 9 files changed, 27 insertions(+), 28 deletions(-) diff --git a/api/src/main/java/org/openmrs/module/billing/api/impl/BillServiceImpl.java b/api/src/main/java/org/openmrs/module/billing/api/impl/BillServiceImpl.java index 375de166..660d788a 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/impl/BillServiceImpl.java +++ b/api/src/main/java/org/openmrs/module/billing/api/impl/BillServiceImpl.java @@ -161,7 +161,7 @@ public Bill save(Bill bill) { } else { billToUpdate.setStatus(BillStatus.PENDING); } - + // Save the updated bill return super.save(billToUpdate); } diff --git a/api/src/main/java/org/openmrs/module/billing/api/search/BillSearch.java b/api/src/main/java/org/openmrs/module/billing/api/search/BillSearch.java index a70ded4e..04fe2786 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/search/BillSearch.java +++ b/api/src/main/java/org/openmrs/module/billing/api/search/BillSearch.java @@ -98,7 +98,7 @@ public void updateCriteria(Criteria criteria) { if (matchingPatients != null && !matchingPatients.isEmpty()) { criteria.add(Restrictions.in("patient", matchingPatients)); } else { - criteria.add(Restrictions.sqlRestriction("1 = 2")); + criteria.add(Restrictions.sqlRestriction("1 = 2")); } } diff --git a/api/src/test/java/org/openmrs/module/billing/impl/BillServiceImplTest.java b/api/src/test/java/org/openmrs/module/billing/impl/BillServiceImplTest.java index 49d5f974..10975392 100644 --- a/api/src/test/java/org/openmrs/module/billing/impl/BillServiceImplTest.java +++ b/api/src/test/java/org/openmrs/module/billing/impl/BillServiceImplTest.java @@ -12,7 +12,6 @@ * Copyright (C) OpenMRS, LLC. All Rights Reserved. */ - package org.openmrs.module.billing.impl; import static org.junit.jupiter.api.Assertions.*; @@ -226,9 +225,9 @@ public void save_shouldUpdateExistingBillWithUpdatedBillItem() { billService.save(pendingBill); Context.flushSession(); - Context.clearSession(); - - Bill updatedBill = billService.getById(2); + Context.clearSession(); + + Bill updatedBill = billService.getById(2); assertEquals(pendingBill, updatedBill); assertEquals(updatedPrice, updatedBill.getLineItems().get(0).getPrice()); diff --git a/api/src/test/resources/org/openmrs/module/billing/api/include/BillTest.xml b/api/src/test/resources/org/openmrs/module/billing/api/include/BillTest.xml index b2fb47b7..6f78be28 100644 --- a/api/src/test/resources/org/openmrs/module/billing/api/include/BillTest.xml +++ b/api/src/test/resources/org/openmrs/module/billing/api/include/BillTest.xml @@ -13,7 +13,7 @@ diff --git a/api/src/test/resources/org/openmrs/module/billing/api/include/PaymentModeTest.xml b/api/src/test/resources/org/openmrs/module/billing/api/include/PaymentModeTest.xml index ebe5d8ac..fc0cfae7 100644 --- a/api/src/test/resources/org/openmrs/module/billing/api/include/PaymentModeTest.xml +++ b/api/src/test/resources/org/openmrs/module/billing/api/include/PaymentModeTest.xml @@ -1,13 +1,13 @@ diff --git a/fhir/src/test/resources/org/openmrs/module/billing/include/PaymentModeTest.xml b/fhir/src/test/resources/org/openmrs/module/billing/include/PaymentModeTest.xml index ebe5d8ac..fc0cfae7 100644 --- a/fhir/src/test/resources/org/openmrs/module/billing/include/PaymentModeTest.xml +++ b/fhir/src/test/resources/org/openmrs/module/billing/include/PaymentModeTest.xml @@ -1,13 +1,13 @@ Date: Wed, 17 Dec 2025 14:55:22 +0300 Subject: [PATCH 11/20] add optional 'forceNewBill' flag to Bill REST API (#9) --- .../module/billing/api/impl/BillServiceImpl.java | 7 +++++++ .../org/openmrs/module/billing/api/model/Bill.java | 10 ++++++++++ .../module/billing/web/rest/resource/BillResource.java | 6 ++++++ 3 files changed, 23 insertions(+) diff --git a/api/src/main/java/org/openmrs/module/billing/api/impl/BillServiceImpl.java b/api/src/main/java/org/openmrs/module/billing/api/impl/BillServiceImpl.java index 660d788a..d8359d58 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/impl/BillServiceImpl.java +++ b/api/src/main/java/org/openmrs/module/billing/api/impl/BillServiceImpl.java @@ -134,6 +134,13 @@ public Bill save(Bill bill) { bill.setReceiptNumber(generator.generateNumber(bill)); } } + + // force new bill save without merging when forceNewBill is set true + if (bill.getForceNewBill() != null && bill.getForceNewBill()) { + // Skip merge logic, just save the bill as new + return super.save(bill); + } + // Check if there is an existing pending bill for the patient List bills = searchBill(bill.getPatient()); if (!bills.isEmpty()) { diff --git a/api/src/main/java/org/openmrs/module/billing/api/model/Bill.java b/api/src/main/java/org/openmrs/module/billing/api/model/Bill.java index 7c502a3f..df2842f3 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/model/Bill.java +++ b/api/src/main/java/org/openmrs/module/billing/api/model/Bill.java @@ -60,6 +60,8 @@ public class Bill extends BaseOpenmrsData { private String adjustmentReason; + private Boolean forceNewBill = false; + public String getAdjustmentReason() { return adjustmentReason; } @@ -258,6 +260,14 @@ public void setPayments(Set payments) { this.payments = payments; } + public Boolean getForceNewBill() { + return forceNewBill; + } + + public void setForceNewBill(Boolean forceNewBill) { + this.forceNewBill = forceNewBill; + } + public Payment addPayment(PaymentMode mode, Set attributes, BigDecimal amount, BigDecimal amountTendered) { if (mode == null) { diff --git a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillResource.java b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillResource.java index 0a4146c9..e745adf0 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillResource.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillResource.java @@ -75,6 +75,7 @@ public DelegatingResourceDescription getRepresentationDescription(Representation description.addProperty("patient", Representation.REF); description.addProperty("payments", Representation.FULL); description.addProperty("receiptNumber"); + description.addProperty("forceNewBill"); description.addProperty("status"); description.addProperty("adjustmentReason"); description.addProperty("id"); @@ -140,6 +141,11 @@ public void setAdjustReason(Bill instance, String adjustReason) { } } + @PropertySetter("forceNewBill") + public void setForceNewBill(Bill instance, Boolean forceNewBill) { + instance.setForceNewBill(forceNewBill != null ? forceNewBill : false); + } + @Override public Bill save(Bill bill) { //TODO: Test all the ways that this could fail From 925684020ce0c2a2221656e19022bc4a5768a416 Mon Sep 17 00:00:00 2001 From: Mutagubya Jonathan Date: Thu, 18 Dec 2025 21:41:25 +0300 Subject: [PATCH 12/20] Add nested REST endpoints for managing bill line items (#12) * Add nested REST endpoints for managing bill line items * remove unused imports --- README.md | 1 + .../api/impl/BillLineItemServiceImpl.java | 8 +- .../module/billing/api/model/Bill.java | 20 ++- .../module/billing/api/model/BillTest.java | 18 +- .../impl/BillLineItemServiceImplTest.java | 27 +-- .../billing/impl/BillServiceImplTest.java | 21 ++- .../resource/BillLineItemNestedResource.java | 162 ++++++++++++++++++ .../web/rest/resource/BillResource.java | 4 +- 8 files changed, 221 insertions(+), 40 deletions(-) create mode 100644 omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillLineItemNestedResource.java diff --git a/README.md b/README.md index 3446c5d0..3989fd84 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,7 @@ The module provides several global properties for configuration: - `billing.adjustmentReasonField`: Require adjustment reason field (true/false) - `billing.autofillPaymentAmount`: Auto-fill payment amount with remaining balance (default: false) - `billing.patientDashboard2BillCount`: Number of bills to show on patient dashboard (default: 5) +- `billing.disableDrugOrderBillAutoCreation` : Disable automatic bill creation for drug orders (true/false) **Financial Reports**: diff --git a/api/src/main/java/org/openmrs/module/billing/api/impl/BillLineItemServiceImpl.java b/api/src/main/java/org/openmrs/module/billing/api/impl/BillLineItemServiceImpl.java index 561360e2..11735bef 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/impl/BillLineItemServiceImpl.java +++ b/api/src/main/java/org/openmrs/module/billing/api/impl/BillLineItemServiceImpl.java @@ -34,9 +34,9 @@ protected IEntityAuthorizationPrivileges getPrivileges() { protected void validate(BillLineItem object) { if (object != null && object.getBill() != null) { Bill bill = object.getBill(); - if (!bill.isPending()) { + if (!bill.editable()) { throw new IllegalStateException( - "Line items can only be modified when the bill is in PENDING state. Current status: " + "Line items can only be modified when the bill is in PENDING or POSTED state. Current status: " + bill.getStatus()); } } @@ -92,9 +92,9 @@ public void purge(BillLineItem entity) { if (entity != null && entity.getBill() != null) { bill = entity.getBill(); // Validate before purging (purge doesn't call validate()) - if (!bill.isPending()) { + if (!bill.editable()) { throw new IllegalStateException( - "Line items can only be modified when the bill is in PENDING state. Current status: " + "Line items can only be modified when the bill is in PENDING or POSTED state. Current status: " + bill.getStatus()); } } diff --git a/api/src/main/java/org/openmrs/module/billing/api/model/Bill.java b/api/src/main/java/org/openmrs/module/billing/api/model/Bill.java index df2842f3..961d1540 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/model/Bill.java +++ b/api/src/main/java/org/openmrs/module/billing/api/model/Bill.java @@ -182,9 +182,9 @@ public void setLineItems(List lineItems) { // Only validate if lineItems is already initialized // This prevents validation during Hibernate entity loading (when lineItems is null) // but still validates user modifications (when lineItems is already set) - if (this.lineItems != null && !isPending()) { + if (this.lineItems != null && !editable()) { throw new IllegalStateException( - "Line items can only be modified when the bill is in PENDING state. Current status: " + "Line items can only be modified when the bill is in PENDING or POSTED state. Current status: " + this.getStatus()); } this.lineItems = lineItems; @@ -225,9 +225,9 @@ public void addLineItem(BillLineItem item) { throw new NullPointerException("The list item to add must be defined."); } - if (!isPending()) { + if (!editable()) { throw new IllegalStateException( - "Line items can only be modified when the bill is in PENDING state. Current status: " + "Line items can only be modified when the bill is in PENDING or POSTED state. Current status: " + this.getStatus()); } @@ -241,9 +241,9 @@ public void addLineItem(BillLineItem item) { public void removeLineItem(BillLineItem item) { if (item != null) { - if (!isPending()) { + if (!editable()) { throw new IllegalStateException( - "Line items can only be modified when the bill is in PENDING state. Current status: " + "Line items can only be modified when the bill is in PENDING or POSTED state. Current status: " + this.getStatus()); } if (this.lineItems != null) { @@ -363,12 +363,14 @@ private void checkAuthorizedToAdjust() { /** * Checks if the bill is in PENDING state. - * + * * @return {@code true} if the bill is new (no ID) or is in PENDING state, {@code false} otherwise */ - public boolean isPending() { + public boolean editable() { // New bills (no ID) are considered pending, existing bills must be in PENDING state - return this.getId() == null || this.getStatus() == BillStatus.PENDING; + // If we do a partial payment bill is set to POSTED status. We should be able to edit posted status too + return getStatus() == null || this.getId() == null || this.getStatus() == BillStatus.PENDING + || this.getStatus() == BillStatus.POSTED; } public void recalculateLineItemOrder() { diff --git a/api/src/test/java/org/openmrs/module/billing/api/model/BillTest.java b/api/src/test/java/org/openmrs/module/billing/api/model/BillTest.java index f34f508a..b8981bc9 100644 --- a/api/src/test/java/org/openmrs/module/billing/api/model/BillTest.java +++ b/api/src/test/java/org/openmrs/module/billing/api/model/BillTest.java @@ -206,8 +206,8 @@ public void addLineItem_shouldAllowAddingLineItemToExistingPendingBill() { assertEquals(1, bill.getLineItems().size()); } - @Test(expected = IllegalStateException.class) - public void addLineItem_shouldThrowExceptionWhenBillIsPosted() { + @Test + public void addLineItem_shouldAllowAddingLineItemToExistingPostedBill() { Bill bill = new Bill(); bill.setId(1); bill.setStatus(BillStatus.POSTED); @@ -217,7 +217,9 @@ public void addLineItem_shouldThrowExceptionWhenBillIsPosted() { lineItem.setPrice(BigDecimal.valueOf(100)); lineItem.setQuantity(1); + // Should not throw exception for POSTED bill bill.addLineItem(lineItem); + assertEquals(1, bill.getLineItems().size()); } @Test(expected = IllegalStateException.class) @@ -265,8 +267,8 @@ public void removeLineItem_shouldAllowRemovingLineItemFromPendingBill() { assertEquals(0, bill.getLineItems().size()); } - @Test(expected = IllegalStateException.class) - public void removeLineItem_shouldThrowExceptionWhenBillIsPosted() { + @Test + public void removeLineItem_shouldAllowRemovingLineItemFromPostedBill() { Bill bill = new Bill(); bill.setId(1); bill.setStatus(BillStatus.POSTED); @@ -277,7 +279,9 @@ public void removeLineItem_shouldThrowExceptionWhenBillIsPosted() { lineItem.setQuantity(1); bill.getLineItems().add(lineItem); + // Should not throw exception for POSTED bill bill.removeLineItem(lineItem); + assertEquals(0, bill.getLineItems().size()); } @Test @@ -313,15 +317,17 @@ public void setLineItems_shouldAllowSettingLineItemsOnExistingPendingBill() { assertEquals(1, bill.getLineItems().size()); } - @Test(expected = IllegalStateException.class) - public void setLineItems_shouldThrowExceptionWhenBillIsPosted() { + @Test + public void setLineItems_shouldAllowSettingLineItemsOnExistingPostedBill() { Bill bill = new Bill(); bill.setId(1); bill.setStatus(BillStatus.POSTED); ArrayList existingLineItems = new ArrayList<>(); bill.setLineItems(existingLineItems); existingLineItems.add(new BillLineItem()); + // Should not throw exception for POSTED bill bill.setLineItems(existingLineItems); + assertEquals(1, bill.getLineItems().size()); } } diff --git a/api/src/test/java/org/openmrs/module/billing/impl/BillLineItemServiceImplTest.java b/api/src/test/java/org/openmrs/module/billing/impl/BillLineItemServiceImplTest.java index cd6e0151..b809c77e 100644 --- a/api/src/test/java/org/openmrs/module/billing/impl/BillLineItemServiceImplTest.java +++ b/api/src/test/java/org/openmrs/module/billing/impl/BillLineItemServiceImplTest.java @@ -73,7 +73,7 @@ public void save_shouldAllowSavingLineItemForPendingBill() { * @see org.openmrs.module.billing.api.impl.BillLineItemServiceImpl#save(BillLineItem) */ @Test - public void save_shouldThrowExceptionWhenSavingLineItemForPostedBill() { + public void save_shouldAllowSavingLineItemForPostedBill() { // Get the POSTED bill from test data (bill_id=0) Bill postedBill = billService.getById(0); assertNotNull(postedBill); @@ -83,11 +83,13 @@ public void save_shouldThrowExceptionWhenSavingLineItemForPostedBill() { BillLineItem lineItem = postedBill.getLineItems().get(0); assertNotNull(lineItem); - // Try to update the line item + // Update the line item lineItem.setPrice(BigDecimal.valueOf(99.99)); - // Should throw exception - assertThrows(IllegalStateException.class, () -> billLineItemService.save(lineItem)); + // Should not throw exception + BillLineItem savedItem = billLineItemService.save(lineItem); + assertNotNull(savedItem); + assertEquals(BigDecimal.valueOf(99.99), savedItem.getPrice()); } /** @@ -136,7 +138,7 @@ public void voidEntity_shouldAllowVoidingLineItemForPendingBill() { * @see org.openmrs.module.billing.api.impl.BillLineItemServiceImpl#voidEntity(BillLineItem, String) */ @Test - public void voidEntity_shouldThrowExceptionWhenVoidingLineItemForPostedBill() { + public void voidEntity_shouldAllowVoidingLineItemForPostedBill() { // Get the POSTED bill from test data (bill_id=0) Bill postedBill = billService.getById(0); assertNotNull(postedBill); @@ -145,9 +147,12 @@ public void voidEntity_shouldThrowExceptionWhenVoidingLineItemForPostedBill() { // Get a line item from the posted bill BillLineItem lineItem = postedBill.getLineItems().get(0); assertNotNull(lineItem); + assertFalse(lineItem.getVoided()); - // Should throw exception - assertThrows(IllegalStateException.class, () -> billLineItemService.voidEntity(lineItem, "Test void reason")); + // Should not throw exception + BillLineItem voidedItem = billLineItemService.voidEntity(lineItem, "Test void reason"); + assertNotNull(voidedItem); + assertTrue(voidedItem.getVoided()); } /** @@ -179,11 +184,11 @@ public void purge_shouldAllowPurgingLineItemForPendingBill() { * @see org.openmrs.module.billing.api.impl.BillLineItemServiceImpl#purge(BillLineItem) */ @Test - public void purge_shouldThrowExceptionWhenPurgingLineItemForPostedBill() { - // Get the POSTED bill from test data (bill_id=0) - Bill postedBill = billService.getById(0); + public void purge_shouldThrowExceptionWhenPurgingLineItemForPaidBill() { + // Get the POSTED bill from test data (bill_id=1) + Bill postedBill = billService.getById(1); assertNotNull(postedBill); - assertEquals(BillStatus.POSTED, postedBill.getStatus()); + assertEquals(BillStatus.PAID, postedBill.getStatus()); // Get a line item from the posted bill BillLineItem lineItem = postedBill.getLineItems().get(0); diff --git a/api/src/test/java/org/openmrs/module/billing/impl/BillServiceImplTest.java b/api/src/test/java/org/openmrs/module/billing/impl/BillServiceImplTest.java index 10975392..3a0880ba 100644 --- a/api/src/test/java/org/openmrs/module/billing/impl/BillServiceImplTest.java +++ b/api/src/test/java/org/openmrs/module/billing/impl/BillServiceImplTest.java @@ -285,19 +285,24 @@ public void save_shouldAllowAddingLineItemsToPendingBill() { * @see org.openmrs.module.billing.api.impl.BillServiceImpl#save(Bill) */ @Test - public void save_shouldThrowExceptionWhenAddingLineItemsToPostedBill() { + public void save_shouldAllowAddingLineItemsToPostedBill() { // Get the POSTED bill from test data (bill_id=0) Bill postedBill = billService.getById(0); assertNotNull(postedBill); assertEquals(BillStatus.POSTED, postedBill.getStatus()); - // Try to add a new line item + // Add a new line item BillLineItem newLineItem = new BillLineItem(); newLineItem.setPrice(BigDecimal.valueOf(25.50)); newLineItem.setQuantity(2); + newLineItem.setPaymentStatus(BillStatus.PENDING); + newLineItem.setLineItemOrder(postedBill.getLineItems().size()); + postedBill.addLineItem(newLineItem); - // Should throw exception - assertThrows(IllegalStateException.class, () -> postedBill.addLineItem(newLineItem)); + // Should not throw exception + Bill savedBill = billService.save(postedBill); + assertNotNull(savedBill); + assertTrue(savedBill.getLineItems().size() > 0); } /** @@ -346,11 +351,11 @@ public void save_shouldAllowRemovingLineItemsFromPendingBill() { * @see org.openmrs.module.billing.api.impl.BillServiceImpl#save(Bill) */ @Test - public void save_shouldThrowExceptionWhenRemovingLineItemsFromPostedBill() { - // Get the POSTED bill from test data (bill_id=0) - Bill postedBill = billService.getById(0); + public void save_Bill_shouldThrowExceptionWhenRemovingLineItemsFromPaidBill() { + // Get the POSTED bill from test data (bill_id=1) + Bill postedBill = billService.getById(1); assertNotNull(postedBill); - assertEquals(BillStatus.POSTED, postedBill.getStatus()); + assertEquals(BillStatus.PAID, postedBill.getStatus()); BillLineItem itemToRemove = postedBill.getLineItems().get(0); diff --git a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillLineItemNestedResource.java b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillLineItemNestedResource.java new file mode 100644 index 00000000..fad6e8c8 --- /dev/null +++ b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillLineItemNestedResource.java @@ -0,0 +1,162 @@ +package org.openmrs.module.billing.web.rest.resource; + +import org.openmrs.api.context.Context; +import org.openmrs.module.billing.api.BillLineItemService; +import org.openmrs.module.billing.api.IBillService; +import org.openmrs.module.billing.api.model.Bill; +import org.openmrs.module.billing.api.model.BillLineItem; + +import org.openmrs.module.billing.api.model.BillStatus; +import org.openmrs.module.webservices.rest.web.RequestContext; +import org.openmrs.module.webservices.rest.web.annotation.PropertySetter; +import org.openmrs.module.webservices.rest.web.annotation.SubResource; +import org.openmrs.module.webservices.rest.web.representation.DefaultRepresentation; +import org.openmrs.module.webservices.rest.web.representation.FullRepresentation; +import org.openmrs.module.webservices.rest.web.representation.Representation; +import org.openmrs.module.webservices.rest.web.resource.api.PageableResult; +import org.openmrs.module.webservices.rest.web.resource.impl.AlreadyPaged; +import org.openmrs.module.webservices.rest.web.resource.impl.DelegatingResourceDescription; +import org.openmrs.module.webservices.rest.web.resource.impl.DelegatingSubResource; +import org.openmrs.module.webservices.rest.web.response.ObjectNotFoundException; +import org.openmrs.module.webservices.rest.web.response.ResponseException; + +import java.math.BigDecimal; +import java.util.ArrayList; + +@SubResource(parent = BillResource.class, path = "lineItem", supportedClass = BillLineItem.class, + supportedOpenmrsVersions = {"2.0 - 2.*"}) +public class BillLineItemNestedResource extends DelegatingSubResource { + + @Override + public DelegatingResourceDescription getRepresentationDescription(Representation rep) { + DelegatingResourceDescription description = new DelegatingResourceDescription(); + if (rep instanceof DefaultRepresentation || rep instanceof FullRepresentation) { + description.addProperty("uuid"); + description.addProperty("quantity"); + description.addProperty("price"); + description.addProperty("lineItemOrder"); + description.addProperty("paymentStatus"); + description.addProperty("item"); + description.addProperty("billableService", Representation.REF); + } + return description; + } + + @Override + public DelegatingResourceDescription getCreatableProperties() { + DelegatingResourceDescription description = new DelegatingResourceDescription(); + description.addProperty("quantity"); + description.addProperty("price"); + description.addProperty("lineItemOrder"); + description.addProperty("paymentStatus"); + return description; + } + + @Override + public BillLineItem save(BillLineItem lineItem) { + IBillService billService = Context.getService(IBillService.class); + Bill bill = lineItem.getBill(); + + // Validate bill is editable only if bill is PENDING or POSTED + if (bill == null || !bill.editable()) { + throw new IllegalArgumentException("Bill is not editable"); + } + + // Save the line item + BillLineItemService service = Context.getService(BillLineItemService.class); + return service.save(lineItem); + } + + @Override + public void delete(String parentUniqueId, final String uuid, String reason, RequestContext context) { + IBillService billService = Context.getService(IBillService.class); + Bill bill = findBill(billService, parentUniqueId); + BillLineItem lineItem = findLineItem(bill, uuid); + + // Void the line item (soft delete) + lineItem.setVoided(true); + lineItem.setVoidReason(reason); + lineItem.setVoidedBy(Context.getAuthenticatedUser()); + + // Save the bill to persist the voided status + billService.save(bill); + } + + @PropertySetter(value = "quantity") + public void setQuantity(BillLineItem instance, Integer quantity) { + instance.setQuantity(quantity); + } + + @PropertySetter(value = "price") + public void setPrice(BillLineItem instance, Object price) { + if (price instanceof Double || price instanceof Integer) { + double priceValue = ((Number) price).doubleValue(); + instance.setPrice(BigDecimal.valueOf(priceValue)); + } else { + throw new IllegalArgumentException("Unsupported price type: " + price.getClass().getName()); + } + } + + @PropertySetter(value = "lineItemOrder") + public void setLineItemOrder(BillLineItem instance, Integer order) { + instance.setLineItemOrder(order); + } + + @PropertySetter(value = "paymentStatus") + public void setPaymentStatus(BillLineItem instance, BillStatus status) { + instance.setPaymentStatus(status); + } + + @Override + public PageableResult doGetAll(Bill parent, RequestContext context) { + return new AlreadyPaged(context, + new ArrayList(parent.getLineItems()), false); + } + + @Override + public BillLineItem getByUniqueId(String uuid) { + return Context.getService(BillLineItemService.class).getByUuid(uuid); + } + + @Override + protected void delete(BillLineItem billLineItem, String s, RequestContext requestContext) throws ResponseException { + + } + + @Override + public void purge(BillLineItem billLineItem, RequestContext requestContext) throws ResponseException { + + } + + @Override + public Bill getParent(BillLineItem instance) { + return instance.getBill(); + } + + @Override + public void setParent(BillLineItem instance, Bill parent) { + instance.setBill(parent); + } + + @Override + public BillLineItem newDelegate() { + return new BillLineItem(); + } + + private Bill findBill(IBillService service, String billUUID) { + Bill bill = service.getByUuid(billUUID); + if (bill == null) { + throw new ObjectNotFoundException(); + } + return bill; + } + + private BillLineItem findLineItem(Bill bill, final String lineItemUUID) { + for (BillLineItem item : bill.getLineItems()) { + if (item != null && item.getUuid().equals(lineItemUUID)) { + return item; + } + } + throw new ObjectNotFoundException(); + } +} \ No newline at end of file diff --git a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillResource.java b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillResource.java index e745adf0..3e04a5e4 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillResource.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillResource.java @@ -91,9 +91,9 @@ public DelegatingResourceDescription getCreatableProperties() { @PropertySetter("lineItems") public void setBillLineItems(Bill instance, List lineItems) { - if (!instance.isPending()) { + if (!instance.editable()) { throw new IllegalStateException( - "Line items can only be modified when the bill is in PENDING state. Current status: " + "Line items can only be modified when the bill is in PENDING or POSTED state. Current status: " + instance.getStatus()); } if (instance.getLineItems() == null) { From e0178e6ab3db9feb651505ccc7fe5fa3fb3a4c95 Mon Sep 17 00:00:00 2001 From: ganeshdevisetti Date: Wed, 24 Dec 2025 19:05:14 +0530 Subject: [PATCH 13/20] Addition of Discount Attributes --- .../billing/api/model/BillLineItem.java | 36 +++++++- api/src/main/resources/Bill.hbm.xml | 2 + .../module/billing/api/model/BillTest.java | 92 +++++++++++++++++++ .../resource/BillLineItemNestedResource.java | 16 ++++ .../rest/resource/BillLineItemResource.java | 13 +++ omod/src/main/resources/liquibase.xml | 12 +++ 6 files changed, 168 insertions(+), 3 deletions(-) diff --git a/api/src/main/java/org/openmrs/module/billing/api/model/BillLineItem.java b/api/src/main/java/org/openmrs/module/billing/api/model/BillLineItem.java index 60cedc09..0c41339c 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/model/BillLineItem.java +++ b/api/src/main/java/org/openmrs/module/billing/api/model/BillLineItem.java @@ -14,7 +14,6 @@ package org.openmrs.module.billing.api.model; import java.math.BigDecimal; -import java.util.Objects; import org.openmrs.BaseChangeableOpenmrsData; import org.openmrs.Order; @@ -51,6 +50,10 @@ public class BillLineItem extends BaseChangeableOpenmrsData { private Order order; + private BigDecimal discount; + + private String discountReason; + @Override public Integer getId() { return billLineItemId; @@ -64,10 +67,21 @@ public void setId(Integer id) { /** * Get the total price for the line item * - * @return double the total price for the line item + * @return BigDecimal the total price for the line item after applying discount */ public BigDecimal getTotal() { - return price.multiply(BigDecimal.valueOf(quantity)); + if (price == null || quantity == null) { + return BigDecimal.ZERO; + } + BigDecimal subtotal = price.multiply(BigDecimal.valueOf(quantity)); + if (discount != null && discount.compareTo(BigDecimal.ZERO) > 0) { + subtotal = subtotal.subtract(discount); + // Ensure total doesn't go negative + if (subtotal.compareTo(BigDecimal.ZERO) < 0) { + subtotal = BigDecimal.ZERO; + } + } + return subtotal; } public CashierItemPrice getItemPrice() { @@ -149,4 +163,20 @@ public Order getOrder() { public void setOrder(Order order) { this.order = order; } + + public BigDecimal getDiscount() { + return discount; + } + + public void setDiscount(BigDecimal discount) { + this.discount = discount; + } + + public String getDiscountReason() { + return discountReason; + } + + public void setDiscountReason(String discountReason) { + this.discountReason = discountReason; + } } diff --git a/api/src/main/resources/Bill.hbm.xml b/api/src/main/resources/Bill.hbm.xml index 18560dc5..f35c821b 100644 --- a/api/src/main/resources/Bill.hbm.xml +++ b/api/src/main/resources/Bill.hbm.xml @@ -169,6 +169,8 @@ 12
+ + diff --git a/api/src/test/java/org/openmrs/module/billing/api/model/BillTest.java b/api/src/test/java/org/openmrs/module/billing/api/model/BillTest.java index b8981bc9..7c4d0ca1 100644 --- a/api/src/test/java/org/openmrs/module/billing/api/model/BillTest.java +++ b/api/src/test/java/org/openmrs/module/billing/api/model/BillTest.java @@ -330,4 +330,96 @@ public void setLineItems_shouldAllowSettingLineItemsOnExistingPostedBill() { assertEquals(1, bill.getLineItems().size()); } + @Test + public void getTotal_shouldCalculateTotalWithDiscount() { + BillLineItem lineItem = new BillLineItem(); + lineItem.setPrice(BigDecimal.valueOf(100)); + lineItem.setQuantity(2); + lineItem.setDiscount(BigDecimal.valueOf(20)); + + // Total should be (100 * 2) - 20 = 180 + assertEquals(BigDecimal.valueOf(180), lineItem.getTotal()); + } + + @Test + public void getTotal_shouldReturnSubtotalWhenDiscountIsNull() { + BillLineItem lineItem = new BillLineItem(); + lineItem.setPrice(BigDecimal.valueOf(100)); + lineItem.setQuantity(2); + lineItem.setDiscount(null); + + // Total should be 100 * 2 = 200 (no discount applied) + assertEquals(BigDecimal.valueOf(200), lineItem.getTotal()); + } + + @Test + public void getTotal_shouldReturnSubtotalWhenDiscountIsZero() { + BillLineItem lineItem = new BillLineItem(); + lineItem.setPrice(BigDecimal.valueOf(100)); + lineItem.setQuantity(2); + lineItem.setDiscount(BigDecimal.ZERO); + + // Total should be 100 * 2 = 200 (zero discount) + assertEquals(BigDecimal.valueOf(200), lineItem.getTotal()); + } + + @Test + public void getTotal_shouldNotReturnNegativeWhenDiscountExceedsSubtotal() { + BillLineItem lineItem = new BillLineItem(); + lineItem.setPrice(BigDecimal.valueOf(100)); + lineItem.setQuantity(1); + lineItem.setDiscount(BigDecimal.valueOf(150)); + + // Total should be 0 (not negative) when discount exceeds subtotal + assertEquals(BigDecimal.ZERO, lineItem.getTotal()); + } + + @Test + public void getTotal_shouldIncludeDiscountedLineItemsInBillTotal() { + Bill bill = new Bill(); + bill.setLineItems(new ArrayList<>()); + + BillLineItem lineItem1 = new BillLineItem(); + lineItem1.setPrice(BigDecimal.valueOf(100)); + lineItem1.setQuantity(2); + lineItem1.setDiscount(BigDecimal.valueOf(20)); + lineItem1.setVoided(false); + bill.getLineItems().add(lineItem1); + + BillLineItem lineItem2 = new BillLineItem(); + lineItem2.setPrice(BigDecimal.valueOf(50)); + lineItem2.setQuantity(1); + lineItem2.setDiscount(null); + lineItem2.setVoided(false); + bill.getLineItems().add(lineItem2); + + // Total should be (100*2 - 20) + (50*1) = 180 + 50 = 230 + assertEquals(BigDecimal.valueOf(230), bill.getTotal()); + } + + @Test + public void synchronizeBillStatus_shouldUpdateStatusCorrectlyWithDiscount() { + Bill bill = new Bill(); + bill.setLineItems(new ArrayList<>()); + bill.setPayments(new HashSet<>()); + + BillLineItem lineItem = new BillLineItem(); + lineItem.setPrice(BigDecimal.valueOf(100)); + lineItem.setQuantity(1); + lineItem.setDiscount(BigDecimal.valueOf(20)); + lineItem.setVoided(false); + bill.getLineItems().add(lineItem); + + // Bill total is 80 (100 - 20) + Payment payment = new Payment(); + payment.setAmountTendered(BigDecimal.valueOf(80)); + payment.setVoided(false); + bill.getPayments().add(payment); + + bill.synchronizeBillStatus(); + + // Should be PAID since payment (80) equals total (80) + assertEquals(BillStatus.PAID, bill.getStatus()); + } + } diff --git a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillLineItemNestedResource.java b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillLineItemNestedResource.java index fad6e8c8..a24fb397 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillLineItemNestedResource.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillLineItemNestedResource.java @@ -38,6 +38,8 @@ public DelegatingResourceDescription getRepresentationDescription(Representation description.addProperty("paymentStatus"); description.addProperty("item"); description.addProperty("billableService", Representation.REF); + description.addProperty("discount"); + description.addProperty("discountReason"); } return description; } @@ -49,6 +51,8 @@ public DelegatingResourceDescription getCreatableProperties() { description.addProperty("price"); description.addProperty("lineItemOrder"); description.addProperty("paymentStatus"); + description.addProperty("discount"); + description.addProperty("discountReason"); return description; } @@ -107,6 +111,18 @@ public void setPaymentStatus(BillLineItem instance, BillStatus status) { instance.setPaymentStatus(status); } + @PropertySetter(value = "discount") + public void setDiscount(BillLineItem instance, Object discount) { + if (discount == null) { + instance.setDiscount(null); + } else if (discount instanceof Double || discount instanceof Integer) { + double discountValue = ((Number) discount).doubleValue(); + instance.setDiscount(BigDecimal.valueOf(discountValue)); + } else { + throw new IllegalArgumentException("Unsupported discount type: " + discount.getClass().getName()); + } + } + @Override public PageableResult doGetAll(Bill parent, RequestContext context) { return new AlreadyPaged(context, diff --git a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillLineItemResource.java b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillLineItemResource.java index 42f4e73d..593092fe 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillLineItemResource.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillLineItemResource.java @@ -59,6 +59,8 @@ public DelegatingResourceDescription getRepresentationDescription(Representation description.addProperty("priceUuid"); description.addProperty("lineItemOrder"); description.addProperty("paymentStatus"); + description.addProperty("discount"); + description.addProperty("discountReason"); return description; } return null; @@ -141,6 +143,17 @@ public String getItemPriceUuid(BillLineItem instance) { } } + @PropertyGetter(value = "discount") + public BigDecimal getDiscount(BillLineItem instance) { + return instance.getDiscount(); + } + + @PropertyGetter(value = "discountReason") + public String getDiscountReason(BillLineItem instance) { + String reason = instance.getDiscountReason(); + return StringUtils.isNotBlank(reason) ? reason : ""; + } + @Override public BillLineItem getByUniqueId(String uuid) { return getService().getByUuid(uuid); diff --git a/omod/src/main/resources/liquibase.xml b/omod/src/main/resources/liquibase.xml index 46c9152c..7abc9306 100644 --- a/omod/src/main/resources/liquibase.xml +++ b/omod/src/main/resources/liquibase.xml @@ -1003,4 +1003,16 @@ + + + Adding discount and discount_reason columns to cashier_bill_line_item table + + + + + + + + + \ No newline at end of file From af1ce88ec2b873a6a782c1c3613b5f576cc0e0d3 Mon Sep 17 00:00:00 2001 From: ganeshdevisetti Date: Sat, 27 Dec 2025 11:05:48 +0530 Subject: [PATCH 14/20] Revert "Add nested REST endpoints for managing bill line items (#12)" This reverts commit 925684020ce0c2a2221656e19022bc4a5768a416. --- README.md | 1 - .../api/impl/BillLineItemServiceImpl.java | 8 +- .../module/billing/api/model/Bill.java | 20 +-- .../module/billing/api/model/BillTest.java | 18 +- .../impl/BillLineItemServiceImplTest.java | 27 ++- .../billing/impl/BillServiceImplTest.java | 21 +-- .../resource/BillLineItemNestedResource.java | 162 ------------------ .../web/rest/resource/BillResource.java | 4 +- 8 files changed, 40 insertions(+), 221 deletions(-) delete mode 100644 omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillLineItemNestedResource.java diff --git a/README.md b/README.md index 3989fd84..3446c5d0 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,6 @@ The module provides several global properties for configuration: - `billing.adjustmentReasonField`: Require adjustment reason field (true/false) - `billing.autofillPaymentAmount`: Auto-fill payment amount with remaining balance (default: false) - `billing.patientDashboard2BillCount`: Number of bills to show on patient dashboard (default: 5) -- `billing.disableDrugOrderBillAutoCreation` : Disable automatic bill creation for drug orders (true/false) **Financial Reports**: diff --git a/api/src/main/java/org/openmrs/module/billing/api/impl/BillLineItemServiceImpl.java b/api/src/main/java/org/openmrs/module/billing/api/impl/BillLineItemServiceImpl.java index 11735bef..561360e2 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/impl/BillLineItemServiceImpl.java +++ b/api/src/main/java/org/openmrs/module/billing/api/impl/BillLineItemServiceImpl.java @@ -34,9 +34,9 @@ protected IEntityAuthorizationPrivileges getPrivileges() { protected void validate(BillLineItem object) { if (object != null && object.getBill() != null) { Bill bill = object.getBill(); - if (!bill.editable()) { + if (!bill.isPending()) { throw new IllegalStateException( - "Line items can only be modified when the bill is in PENDING or POSTED state. Current status: " + "Line items can only be modified when the bill is in PENDING state. Current status: " + bill.getStatus()); } } @@ -92,9 +92,9 @@ public void purge(BillLineItem entity) { if (entity != null && entity.getBill() != null) { bill = entity.getBill(); // Validate before purging (purge doesn't call validate()) - if (!bill.editable()) { + if (!bill.isPending()) { throw new IllegalStateException( - "Line items can only be modified when the bill is in PENDING or POSTED state. Current status: " + "Line items can only be modified when the bill is in PENDING state. Current status: " + bill.getStatus()); } } diff --git a/api/src/main/java/org/openmrs/module/billing/api/model/Bill.java b/api/src/main/java/org/openmrs/module/billing/api/model/Bill.java index 961d1540..df2842f3 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/model/Bill.java +++ b/api/src/main/java/org/openmrs/module/billing/api/model/Bill.java @@ -182,9 +182,9 @@ public void setLineItems(List lineItems) { // Only validate if lineItems is already initialized // This prevents validation during Hibernate entity loading (when lineItems is null) // but still validates user modifications (when lineItems is already set) - if (this.lineItems != null && !editable()) { + if (this.lineItems != null && !isPending()) { throw new IllegalStateException( - "Line items can only be modified when the bill is in PENDING or POSTED state. Current status: " + "Line items can only be modified when the bill is in PENDING state. Current status: " + this.getStatus()); } this.lineItems = lineItems; @@ -225,9 +225,9 @@ public void addLineItem(BillLineItem item) { throw new NullPointerException("The list item to add must be defined."); } - if (!editable()) { + if (!isPending()) { throw new IllegalStateException( - "Line items can only be modified when the bill is in PENDING or POSTED state. Current status: " + "Line items can only be modified when the bill is in PENDING state. Current status: " + this.getStatus()); } @@ -241,9 +241,9 @@ public void addLineItem(BillLineItem item) { public void removeLineItem(BillLineItem item) { if (item != null) { - if (!editable()) { + if (!isPending()) { throw new IllegalStateException( - "Line items can only be modified when the bill is in PENDING or POSTED state. Current status: " + "Line items can only be modified when the bill is in PENDING state. Current status: " + this.getStatus()); } if (this.lineItems != null) { @@ -363,14 +363,12 @@ private void checkAuthorizedToAdjust() { /** * Checks if the bill is in PENDING state. - * + * * @return {@code true} if the bill is new (no ID) or is in PENDING state, {@code false} otherwise */ - public boolean editable() { + public boolean isPending() { // New bills (no ID) are considered pending, existing bills must be in PENDING state - // If we do a partial payment bill is set to POSTED status. We should be able to edit posted status too - return getStatus() == null || this.getId() == null || this.getStatus() == BillStatus.PENDING - || this.getStatus() == BillStatus.POSTED; + return this.getId() == null || this.getStatus() == BillStatus.PENDING; } public void recalculateLineItemOrder() { diff --git a/api/src/test/java/org/openmrs/module/billing/api/model/BillTest.java b/api/src/test/java/org/openmrs/module/billing/api/model/BillTest.java index b8981bc9..f34f508a 100644 --- a/api/src/test/java/org/openmrs/module/billing/api/model/BillTest.java +++ b/api/src/test/java/org/openmrs/module/billing/api/model/BillTest.java @@ -206,8 +206,8 @@ public void addLineItem_shouldAllowAddingLineItemToExistingPendingBill() { assertEquals(1, bill.getLineItems().size()); } - @Test - public void addLineItem_shouldAllowAddingLineItemToExistingPostedBill() { + @Test(expected = IllegalStateException.class) + public void addLineItem_shouldThrowExceptionWhenBillIsPosted() { Bill bill = new Bill(); bill.setId(1); bill.setStatus(BillStatus.POSTED); @@ -217,9 +217,7 @@ public void addLineItem_shouldAllowAddingLineItemToExistingPostedBill() { lineItem.setPrice(BigDecimal.valueOf(100)); lineItem.setQuantity(1); - // Should not throw exception for POSTED bill bill.addLineItem(lineItem); - assertEquals(1, bill.getLineItems().size()); } @Test(expected = IllegalStateException.class) @@ -267,8 +265,8 @@ public void removeLineItem_shouldAllowRemovingLineItemFromPendingBill() { assertEquals(0, bill.getLineItems().size()); } - @Test - public void removeLineItem_shouldAllowRemovingLineItemFromPostedBill() { + @Test(expected = IllegalStateException.class) + public void removeLineItem_shouldThrowExceptionWhenBillIsPosted() { Bill bill = new Bill(); bill.setId(1); bill.setStatus(BillStatus.POSTED); @@ -279,9 +277,7 @@ public void removeLineItem_shouldAllowRemovingLineItemFromPostedBill() { lineItem.setQuantity(1); bill.getLineItems().add(lineItem); - // Should not throw exception for POSTED bill bill.removeLineItem(lineItem); - assertEquals(0, bill.getLineItems().size()); } @Test @@ -317,17 +313,15 @@ public void setLineItems_shouldAllowSettingLineItemsOnExistingPendingBill() { assertEquals(1, bill.getLineItems().size()); } - @Test - public void setLineItems_shouldAllowSettingLineItemsOnExistingPostedBill() { + @Test(expected = IllegalStateException.class) + public void setLineItems_shouldThrowExceptionWhenBillIsPosted() { Bill bill = new Bill(); bill.setId(1); bill.setStatus(BillStatus.POSTED); ArrayList existingLineItems = new ArrayList<>(); bill.setLineItems(existingLineItems); existingLineItems.add(new BillLineItem()); - // Should not throw exception for POSTED bill bill.setLineItems(existingLineItems); - assertEquals(1, bill.getLineItems().size()); } } diff --git a/api/src/test/java/org/openmrs/module/billing/impl/BillLineItemServiceImplTest.java b/api/src/test/java/org/openmrs/module/billing/impl/BillLineItemServiceImplTest.java index b809c77e..cd6e0151 100644 --- a/api/src/test/java/org/openmrs/module/billing/impl/BillLineItemServiceImplTest.java +++ b/api/src/test/java/org/openmrs/module/billing/impl/BillLineItemServiceImplTest.java @@ -73,7 +73,7 @@ public void save_shouldAllowSavingLineItemForPendingBill() { * @see org.openmrs.module.billing.api.impl.BillLineItemServiceImpl#save(BillLineItem) */ @Test - public void save_shouldAllowSavingLineItemForPostedBill() { + public void save_shouldThrowExceptionWhenSavingLineItemForPostedBill() { // Get the POSTED bill from test data (bill_id=0) Bill postedBill = billService.getById(0); assertNotNull(postedBill); @@ -83,13 +83,11 @@ public void save_shouldAllowSavingLineItemForPostedBill() { BillLineItem lineItem = postedBill.getLineItems().get(0); assertNotNull(lineItem); - // Update the line item + // Try to update the line item lineItem.setPrice(BigDecimal.valueOf(99.99)); - // Should not throw exception - BillLineItem savedItem = billLineItemService.save(lineItem); - assertNotNull(savedItem); - assertEquals(BigDecimal.valueOf(99.99), savedItem.getPrice()); + // Should throw exception + assertThrows(IllegalStateException.class, () -> billLineItemService.save(lineItem)); } /** @@ -138,7 +136,7 @@ public void voidEntity_shouldAllowVoidingLineItemForPendingBill() { * @see org.openmrs.module.billing.api.impl.BillLineItemServiceImpl#voidEntity(BillLineItem, String) */ @Test - public void voidEntity_shouldAllowVoidingLineItemForPostedBill() { + public void voidEntity_shouldThrowExceptionWhenVoidingLineItemForPostedBill() { // Get the POSTED bill from test data (bill_id=0) Bill postedBill = billService.getById(0); assertNotNull(postedBill); @@ -147,12 +145,9 @@ public void voidEntity_shouldAllowVoidingLineItemForPostedBill() { // Get a line item from the posted bill BillLineItem lineItem = postedBill.getLineItems().get(0); assertNotNull(lineItem); - assertFalse(lineItem.getVoided()); - // Should not throw exception - BillLineItem voidedItem = billLineItemService.voidEntity(lineItem, "Test void reason"); - assertNotNull(voidedItem); - assertTrue(voidedItem.getVoided()); + // Should throw exception + assertThrows(IllegalStateException.class, () -> billLineItemService.voidEntity(lineItem, "Test void reason")); } /** @@ -184,11 +179,11 @@ public void purge_shouldAllowPurgingLineItemForPendingBill() { * @see org.openmrs.module.billing.api.impl.BillLineItemServiceImpl#purge(BillLineItem) */ @Test - public void purge_shouldThrowExceptionWhenPurgingLineItemForPaidBill() { - // Get the POSTED bill from test data (bill_id=1) - Bill postedBill = billService.getById(1); + public void purge_shouldThrowExceptionWhenPurgingLineItemForPostedBill() { + // Get the POSTED bill from test data (bill_id=0) + Bill postedBill = billService.getById(0); assertNotNull(postedBill); - assertEquals(BillStatus.PAID, postedBill.getStatus()); + assertEquals(BillStatus.POSTED, postedBill.getStatus()); // Get a line item from the posted bill BillLineItem lineItem = postedBill.getLineItems().get(0); diff --git a/api/src/test/java/org/openmrs/module/billing/impl/BillServiceImplTest.java b/api/src/test/java/org/openmrs/module/billing/impl/BillServiceImplTest.java index 3a0880ba..10975392 100644 --- a/api/src/test/java/org/openmrs/module/billing/impl/BillServiceImplTest.java +++ b/api/src/test/java/org/openmrs/module/billing/impl/BillServiceImplTest.java @@ -285,24 +285,19 @@ public void save_shouldAllowAddingLineItemsToPendingBill() { * @see org.openmrs.module.billing.api.impl.BillServiceImpl#save(Bill) */ @Test - public void save_shouldAllowAddingLineItemsToPostedBill() { + public void save_shouldThrowExceptionWhenAddingLineItemsToPostedBill() { // Get the POSTED bill from test data (bill_id=0) Bill postedBill = billService.getById(0); assertNotNull(postedBill); assertEquals(BillStatus.POSTED, postedBill.getStatus()); - // Add a new line item + // Try to add a new line item BillLineItem newLineItem = new BillLineItem(); newLineItem.setPrice(BigDecimal.valueOf(25.50)); newLineItem.setQuantity(2); - newLineItem.setPaymentStatus(BillStatus.PENDING); - newLineItem.setLineItemOrder(postedBill.getLineItems().size()); - postedBill.addLineItem(newLineItem); - // Should not throw exception - Bill savedBill = billService.save(postedBill); - assertNotNull(savedBill); - assertTrue(savedBill.getLineItems().size() > 0); + // Should throw exception + assertThrows(IllegalStateException.class, () -> postedBill.addLineItem(newLineItem)); } /** @@ -351,11 +346,11 @@ public void save_shouldAllowRemovingLineItemsFromPendingBill() { * @see org.openmrs.module.billing.api.impl.BillServiceImpl#save(Bill) */ @Test - public void save_Bill_shouldThrowExceptionWhenRemovingLineItemsFromPaidBill() { - // Get the POSTED bill from test data (bill_id=1) - Bill postedBill = billService.getById(1); + public void save_shouldThrowExceptionWhenRemovingLineItemsFromPostedBill() { + // Get the POSTED bill from test data (bill_id=0) + Bill postedBill = billService.getById(0); assertNotNull(postedBill); - assertEquals(BillStatus.PAID, postedBill.getStatus()); + assertEquals(BillStatus.POSTED, postedBill.getStatus()); BillLineItem itemToRemove = postedBill.getLineItems().get(0); diff --git a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillLineItemNestedResource.java b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillLineItemNestedResource.java deleted file mode 100644 index fad6e8c8..00000000 --- a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillLineItemNestedResource.java +++ /dev/null @@ -1,162 +0,0 @@ -package org.openmrs.module.billing.web.rest.resource; - -import org.openmrs.api.context.Context; -import org.openmrs.module.billing.api.BillLineItemService; -import org.openmrs.module.billing.api.IBillService; -import org.openmrs.module.billing.api.model.Bill; -import org.openmrs.module.billing.api.model.BillLineItem; - -import org.openmrs.module.billing.api.model.BillStatus; -import org.openmrs.module.webservices.rest.web.RequestContext; -import org.openmrs.module.webservices.rest.web.annotation.PropertySetter; -import org.openmrs.module.webservices.rest.web.annotation.SubResource; -import org.openmrs.module.webservices.rest.web.representation.DefaultRepresentation; -import org.openmrs.module.webservices.rest.web.representation.FullRepresentation; -import org.openmrs.module.webservices.rest.web.representation.Representation; -import org.openmrs.module.webservices.rest.web.resource.api.PageableResult; -import org.openmrs.module.webservices.rest.web.resource.impl.AlreadyPaged; -import org.openmrs.module.webservices.rest.web.resource.impl.DelegatingResourceDescription; -import org.openmrs.module.webservices.rest.web.resource.impl.DelegatingSubResource; -import org.openmrs.module.webservices.rest.web.response.ObjectNotFoundException; -import org.openmrs.module.webservices.rest.web.response.ResponseException; - -import java.math.BigDecimal; -import java.util.ArrayList; - -@SubResource(parent = BillResource.class, path = "lineItem", supportedClass = BillLineItem.class, - supportedOpenmrsVersions = {"2.0 - 2.*"}) -public class BillLineItemNestedResource extends DelegatingSubResource { - - @Override - public DelegatingResourceDescription getRepresentationDescription(Representation rep) { - DelegatingResourceDescription description = new DelegatingResourceDescription(); - if (rep instanceof DefaultRepresentation || rep instanceof FullRepresentation) { - description.addProperty("uuid"); - description.addProperty("quantity"); - description.addProperty("price"); - description.addProperty("lineItemOrder"); - description.addProperty("paymentStatus"); - description.addProperty("item"); - description.addProperty("billableService", Representation.REF); - } - return description; - } - - @Override - public DelegatingResourceDescription getCreatableProperties() { - DelegatingResourceDescription description = new DelegatingResourceDescription(); - description.addProperty("quantity"); - description.addProperty("price"); - description.addProperty("lineItemOrder"); - description.addProperty("paymentStatus"); - return description; - } - - @Override - public BillLineItem save(BillLineItem lineItem) { - IBillService billService = Context.getService(IBillService.class); - Bill bill = lineItem.getBill(); - - // Validate bill is editable only if bill is PENDING or POSTED - if (bill == null || !bill.editable()) { - throw new IllegalArgumentException("Bill is not editable"); - } - - // Save the line item - BillLineItemService service = Context.getService(BillLineItemService.class); - return service.save(lineItem); - } - - @Override - public void delete(String parentUniqueId, final String uuid, String reason, RequestContext context) { - IBillService billService = Context.getService(IBillService.class); - Bill bill = findBill(billService, parentUniqueId); - BillLineItem lineItem = findLineItem(bill, uuid); - - // Void the line item (soft delete) - lineItem.setVoided(true); - lineItem.setVoidReason(reason); - lineItem.setVoidedBy(Context.getAuthenticatedUser()); - - // Save the bill to persist the voided status - billService.save(bill); - } - - @PropertySetter(value = "quantity") - public void setQuantity(BillLineItem instance, Integer quantity) { - instance.setQuantity(quantity); - } - - @PropertySetter(value = "price") - public void setPrice(BillLineItem instance, Object price) { - if (price instanceof Double || price instanceof Integer) { - double priceValue = ((Number) price).doubleValue(); - instance.setPrice(BigDecimal.valueOf(priceValue)); - } else { - throw new IllegalArgumentException("Unsupported price type: " + price.getClass().getName()); - } - } - - @PropertySetter(value = "lineItemOrder") - public void setLineItemOrder(BillLineItem instance, Integer order) { - instance.setLineItemOrder(order); - } - - @PropertySetter(value = "paymentStatus") - public void setPaymentStatus(BillLineItem instance, BillStatus status) { - instance.setPaymentStatus(status); - } - - @Override - public PageableResult doGetAll(Bill parent, RequestContext context) { - return new AlreadyPaged(context, - new ArrayList(parent.getLineItems()), false); - } - - @Override - public BillLineItem getByUniqueId(String uuid) { - return Context.getService(BillLineItemService.class).getByUuid(uuid); - } - - @Override - protected void delete(BillLineItem billLineItem, String s, RequestContext requestContext) throws ResponseException { - - } - - @Override - public void purge(BillLineItem billLineItem, RequestContext requestContext) throws ResponseException { - - } - - @Override - public Bill getParent(BillLineItem instance) { - return instance.getBill(); - } - - @Override - public void setParent(BillLineItem instance, Bill parent) { - instance.setBill(parent); - } - - @Override - public BillLineItem newDelegate() { - return new BillLineItem(); - } - - private Bill findBill(IBillService service, String billUUID) { - Bill bill = service.getByUuid(billUUID); - if (bill == null) { - throw new ObjectNotFoundException(); - } - return bill; - } - - private BillLineItem findLineItem(Bill bill, final String lineItemUUID) { - for (BillLineItem item : bill.getLineItems()) { - if (item != null && item.getUuid().equals(lineItemUUID)) { - return item; - } - } - throw new ObjectNotFoundException(); - } -} \ No newline at end of file diff --git a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillResource.java b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillResource.java index 3e04a5e4..e745adf0 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillResource.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillResource.java @@ -91,9 +91,9 @@ public DelegatingResourceDescription getCreatableProperties() { @PropertySetter("lineItems") public void setBillLineItems(Bill instance, List lineItems) { - if (!instance.editable()) { + if (!instance.isPending()) { throw new IllegalStateException( - "Line items can only be modified when the bill is in PENDING or POSTED state. Current status: " + "Line items can only be modified when the bill is in PENDING state. Current status: " + instance.getStatus()); } if (instance.getLineItems() == null) { From f39f9cd3dfe1e3543656129450eb0e57399f5c21 Mon Sep 17 00:00:00 2001 From: ganeshdevisetti Date: Sat, 27 Dec 2025 11:25:25 +0530 Subject: [PATCH 15/20] Revert "Addition of Discount Attributes" This reverts commit e0178e6ab3db9feb651505ccc7fe5fa3fb3a4c95. --- .../billing/api/model/BillLineItem.java | 36 +------- api/src/main/resources/Bill.hbm.xml | 2 - .../module/billing/api/model/BillTest.java | 92 ------------------- .../resource/BillLineItemNestedResource.java | 16 ---- .../rest/resource/BillLineItemResource.java | 13 --- omod/src/main/resources/liquibase.xml | 12 --- 6 files changed, 3 insertions(+), 168 deletions(-) diff --git a/api/src/main/java/org/openmrs/module/billing/api/model/BillLineItem.java b/api/src/main/java/org/openmrs/module/billing/api/model/BillLineItem.java index 0c41339c..60cedc09 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/model/BillLineItem.java +++ b/api/src/main/java/org/openmrs/module/billing/api/model/BillLineItem.java @@ -14,6 +14,7 @@ package org.openmrs.module.billing.api.model; import java.math.BigDecimal; +import java.util.Objects; import org.openmrs.BaseChangeableOpenmrsData; import org.openmrs.Order; @@ -50,10 +51,6 @@ public class BillLineItem extends BaseChangeableOpenmrsData { private Order order; - private BigDecimal discount; - - private String discountReason; - @Override public Integer getId() { return billLineItemId; @@ -67,21 +64,10 @@ public void setId(Integer id) { /** * Get the total price for the line item * - * @return BigDecimal the total price for the line item after applying discount + * @return double the total price for the line item */ public BigDecimal getTotal() { - if (price == null || quantity == null) { - return BigDecimal.ZERO; - } - BigDecimal subtotal = price.multiply(BigDecimal.valueOf(quantity)); - if (discount != null && discount.compareTo(BigDecimal.ZERO) > 0) { - subtotal = subtotal.subtract(discount); - // Ensure total doesn't go negative - if (subtotal.compareTo(BigDecimal.ZERO) < 0) { - subtotal = BigDecimal.ZERO; - } - } - return subtotal; + return price.multiply(BigDecimal.valueOf(quantity)); } public CashierItemPrice getItemPrice() { @@ -163,20 +149,4 @@ public Order getOrder() { public void setOrder(Order order) { this.order = order; } - - public BigDecimal getDiscount() { - return discount; - } - - public void setDiscount(BigDecimal discount) { - this.discount = discount; - } - - public String getDiscountReason() { - return discountReason; - } - - public void setDiscountReason(String discountReason) { - this.discountReason = discountReason; - } } diff --git a/api/src/main/resources/Bill.hbm.xml b/api/src/main/resources/Bill.hbm.xml index f35c821b..18560dc5 100644 --- a/api/src/main/resources/Bill.hbm.xml +++ b/api/src/main/resources/Bill.hbm.xml @@ -169,8 +169,6 @@ 12 - - diff --git a/api/src/test/java/org/openmrs/module/billing/api/model/BillTest.java b/api/src/test/java/org/openmrs/module/billing/api/model/BillTest.java index 7c4d0ca1..b8981bc9 100644 --- a/api/src/test/java/org/openmrs/module/billing/api/model/BillTest.java +++ b/api/src/test/java/org/openmrs/module/billing/api/model/BillTest.java @@ -330,96 +330,4 @@ public void setLineItems_shouldAllowSettingLineItemsOnExistingPostedBill() { assertEquals(1, bill.getLineItems().size()); } - @Test - public void getTotal_shouldCalculateTotalWithDiscount() { - BillLineItem lineItem = new BillLineItem(); - lineItem.setPrice(BigDecimal.valueOf(100)); - lineItem.setQuantity(2); - lineItem.setDiscount(BigDecimal.valueOf(20)); - - // Total should be (100 * 2) - 20 = 180 - assertEquals(BigDecimal.valueOf(180), lineItem.getTotal()); - } - - @Test - public void getTotal_shouldReturnSubtotalWhenDiscountIsNull() { - BillLineItem lineItem = new BillLineItem(); - lineItem.setPrice(BigDecimal.valueOf(100)); - lineItem.setQuantity(2); - lineItem.setDiscount(null); - - // Total should be 100 * 2 = 200 (no discount applied) - assertEquals(BigDecimal.valueOf(200), lineItem.getTotal()); - } - - @Test - public void getTotal_shouldReturnSubtotalWhenDiscountIsZero() { - BillLineItem lineItem = new BillLineItem(); - lineItem.setPrice(BigDecimal.valueOf(100)); - lineItem.setQuantity(2); - lineItem.setDiscount(BigDecimal.ZERO); - - // Total should be 100 * 2 = 200 (zero discount) - assertEquals(BigDecimal.valueOf(200), lineItem.getTotal()); - } - - @Test - public void getTotal_shouldNotReturnNegativeWhenDiscountExceedsSubtotal() { - BillLineItem lineItem = new BillLineItem(); - lineItem.setPrice(BigDecimal.valueOf(100)); - lineItem.setQuantity(1); - lineItem.setDiscount(BigDecimal.valueOf(150)); - - // Total should be 0 (not negative) when discount exceeds subtotal - assertEquals(BigDecimal.ZERO, lineItem.getTotal()); - } - - @Test - public void getTotal_shouldIncludeDiscountedLineItemsInBillTotal() { - Bill bill = new Bill(); - bill.setLineItems(new ArrayList<>()); - - BillLineItem lineItem1 = new BillLineItem(); - lineItem1.setPrice(BigDecimal.valueOf(100)); - lineItem1.setQuantity(2); - lineItem1.setDiscount(BigDecimal.valueOf(20)); - lineItem1.setVoided(false); - bill.getLineItems().add(lineItem1); - - BillLineItem lineItem2 = new BillLineItem(); - lineItem2.setPrice(BigDecimal.valueOf(50)); - lineItem2.setQuantity(1); - lineItem2.setDiscount(null); - lineItem2.setVoided(false); - bill.getLineItems().add(lineItem2); - - // Total should be (100*2 - 20) + (50*1) = 180 + 50 = 230 - assertEquals(BigDecimal.valueOf(230), bill.getTotal()); - } - - @Test - public void synchronizeBillStatus_shouldUpdateStatusCorrectlyWithDiscount() { - Bill bill = new Bill(); - bill.setLineItems(new ArrayList<>()); - bill.setPayments(new HashSet<>()); - - BillLineItem lineItem = new BillLineItem(); - lineItem.setPrice(BigDecimal.valueOf(100)); - lineItem.setQuantity(1); - lineItem.setDiscount(BigDecimal.valueOf(20)); - lineItem.setVoided(false); - bill.getLineItems().add(lineItem); - - // Bill total is 80 (100 - 20) - Payment payment = new Payment(); - payment.setAmountTendered(BigDecimal.valueOf(80)); - payment.setVoided(false); - bill.getPayments().add(payment); - - bill.synchronizeBillStatus(); - - // Should be PAID since payment (80) equals total (80) - assertEquals(BillStatus.PAID, bill.getStatus()); - } - } diff --git a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillLineItemNestedResource.java b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillLineItemNestedResource.java index a24fb397..fad6e8c8 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillLineItemNestedResource.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillLineItemNestedResource.java @@ -38,8 +38,6 @@ public DelegatingResourceDescription getRepresentationDescription(Representation description.addProperty("paymentStatus"); description.addProperty("item"); description.addProperty("billableService", Representation.REF); - description.addProperty("discount"); - description.addProperty("discountReason"); } return description; } @@ -51,8 +49,6 @@ public DelegatingResourceDescription getCreatableProperties() { description.addProperty("price"); description.addProperty("lineItemOrder"); description.addProperty("paymentStatus"); - description.addProperty("discount"); - description.addProperty("discountReason"); return description; } @@ -111,18 +107,6 @@ public void setPaymentStatus(BillLineItem instance, BillStatus status) { instance.setPaymentStatus(status); } - @PropertySetter(value = "discount") - public void setDiscount(BillLineItem instance, Object discount) { - if (discount == null) { - instance.setDiscount(null); - } else if (discount instanceof Double || discount instanceof Integer) { - double discountValue = ((Number) discount).doubleValue(); - instance.setDiscount(BigDecimal.valueOf(discountValue)); - } else { - throw new IllegalArgumentException("Unsupported discount type: " + discount.getClass().getName()); - } - } - @Override public PageableResult doGetAll(Bill parent, RequestContext context) { return new AlreadyPaged(context, diff --git a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillLineItemResource.java b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillLineItemResource.java index 593092fe..42f4e73d 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillLineItemResource.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillLineItemResource.java @@ -59,8 +59,6 @@ public DelegatingResourceDescription getRepresentationDescription(Representation description.addProperty("priceUuid"); description.addProperty("lineItemOrder"); description.addProperty("paymentStatus"); - description.addProperty("discount"); - description.addProperty("discountReason"); return description; } return null; @@ -143,17 +141,6 @@ public String getItemPriceUuid(BillLineItem instance) { } } - @PropertyGetter(value = "discount") - public BigDecimal getDiscount(BillLineItem instance) { - return instance.getDiscount(); - } - - @PropertyGetter(value = "discountReason") - public String getDiscountReason(BillLineItem instance) { - String reason = instance.getDiscountReason(); - return StringUtils.isNotBlank(reason) ? reason : ""; - } - @Override public BillLineItem getByUniqueId(String uuid) { return getService().getByUuid(uuid); diff --git a/omod/src/main/resources/liquibase.xml b/omod/src/main/resources/liquibase.xml index 7abc9306..46c9152c 100644 --- a/omod/src/main/resources/liquibase.xml +++ b/omod/src/main/resources/liquibase.xml @@ -1003,16 +1003,4 @@ - - - Adding discount and discount_reason columns to cashier_bill_line_item table - - - - - - - - - \ No newline at end of file From 3aaedea58fb521aa361dd0c9a0c79368e1d369d9 Mon Sep 17 00:00:00 2001 From: ganeshdevisetti Date: Sat, 27 Dec 2025 11:27:41 +0530 Subject: [PATCH 16/20] Revert "Add nested REST endpoints for managing bill line items (#12)" This reverts commit 925684020ce0c2a2221656e19022bc4a5768a416. --- README.md | 1 - .../api/impl/BillLineItemServiceImpl.java | 8 +- .../module/billing/api/model/Bill.java | 20 +-- .../module/billing/api/model/BillTest.java | 18 +- .../impl/BillLineItemServiceImplTest.java | 27 ++- .../billing/impl/BillServiceImplTest.java | 21 +-- .../resource/BillLineItemNestedResource.java | 162 ------------------ .../web/rest/resource/BillResource.java | 4 +- 8 files changed, 40 insertions(+), 221 deletions(-) delete mode 100644 omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillLineItemNestedResource.java diff --git a/README.md b/README.md index 3989fd84..3446c5d0 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,6 @@ The module provides several global properties for configuration: - `billing.adjustmentReasonField`: Require adjustment reason field (true/false) - `billing.autofillPaymentAmount`: Auto-fill payment amount with remaining balance (default: false) - `billing.patientDashboard2BillCount`: Number of bills to show on patient dashboard (default: 5) -- `billing.disableDrugOrderBillAutoCreation` : Disable automatic bill creation for drug orders (true/false) **Financial Reports**: diff --git a/api/src/main/java/org/openmrs/module/billing/api/impl/BillLineItemServiceImpl.java b/api/src/main/java/org/openmrs/module/billing/api/impl/BillLineItemServiceImpl.java index 11735bef..561360e2 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/impl/BillLineItemServiceImpl.java +++ b/api/src/main/java/org/openmrs/module/billing/api/impl/BillLineItemServiceImpl.java @@ -34,9 +34,9 @@ protected IEntityAuthorizationPrivileges getPrivileges() { protected void validate(BillLineItem object) { if (object != null && object.getBill() != null) { Bill bill = object.getBill(); - if (!bill.editable()) { + if (!bill.isPending()) { throw new IllegalStateException( - "Line items can only be modified when the bill is in PENDING or POSTED state. Current status: " + "Line items can only be modified when the bill is in PENDING state. Current status: " + bill.getStatus()); } } @@ -92,9 +92,9 @@ public void purge(BillLineItem entity) { if (entity != null && entity.getBill() != null) { bill = entity.getBill(); // Validate before purging (purge doesn't call validate()) - if (!bill.editable()) { + if (!bill.isPending()) { throw new IllegalStateException( - "Line items can only be modified when the bill is in PENDING or POSTED state. Current status: " + "Line items can only be modified when the bill is in PENDING state. Current status: " + bill.getStatus()); } } diff --git a/api/src/main/java/org/openmrs/module/billing/api/model/Bill.java b/api/src/main/java/org/openmrs/module/billing/api/model/Bill.java index 961d1540..df2842f3 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/model/Bill.java +++ b/api/src/main/java/org/openmrs/module/billing/api/model/Bill.java @@ -182,9 +182,9 @@ public void setLineItems(List lineItems) { // Only validate if lineItems is already initialized // This prevents validation during Hibernate entity loading (when lineItems is null) // but still validates user modifications (when lineItems is already set) - if (this.lineItems != null && !editable()) { + if (this.lineItems != null && !isPending()) { throw new IllegalStateException( - "Line items can only be modified when the bill is in PENDING or POSTED state. Current status: " + "Line items can only be modified when the bill is in PENDING state. Current status: " + this.getStatus()); } this.lineItems = lineItems; @@ -225,9 +225,9 @@ public void addLineItem(BillLineItem item) { throw new NullPointerException("The list item to add must be defined."); } - if (!editable()) { + if (!isPending()) { throw new IllegalStateException( - "Line items can only be modified when the bill is in PENDING or POSTED state. Current status: " + "Line items can only be modified when the bill is in PENDING state. Current status: " + this.getStatus()); } @@ -241,9 +241,9 @@ public void addLineItem(BillLineItem item) { public void removeLineItem(BillLineItem item) { if (item != null) { - if (!editable()) { + if (!isPending()) { throw new IllegalStateException( - "Line items can only be modified when the bill is in PENDING or POSTED state. Current status: " + "Line items can only be modified when the bill is in PENDING state. Current status: " + this.getStatus()); } if (this.lineItems != null) { @@ -363,14 +363,12 @@ private void checkAuthorizedToAdjust() { /** * Checks if the bill is in PENDING state. - * + * * @return {@code true} if the bill is new (no ID) or is in PENDING state, {@code false} otherwise */ - public boolean editable() { + public boolean isPending() { // New bills (no ID) are considered pending, existing bills must be in PENDING state - // If we do a partial payment bill is set to POSTED status. We should be able to edit posted status too - return getStatus() == null || this.getId() == null || this.getStatus() == BillStatus.PENDING - || this.getStatus() == BillStatus.POSTED; + return this.getId() == null || this.getStatus() == BillStatus.PENDING; } public void recalculateLineItemOrder() { diff --git a/api/src/test/java/org/openmrs/module/billing/api/model/BillTest.java b/api/src/test/java/org/openmrs/module/billing/api/model/BillTest.java index b8981bc9..f34f508a 100644 --- a/api/src/test/java/org/openmrs/module/billing/api/model/BillTest.java +++ b/api/src/test/java/org/openmrs/module/billing/api/model/BillTest.java @@ -206,8 +206,8 @@ public void addLineItem_shouldAllowAddingLineItemToExistingPendingBill() { assertEquals(1, bill.getLineItems().size()); } - @Test - public void addLineItem_shouldAllowAddingLineItemToExistingPostedBill() { + @Test(expected = IllegalStateException.class) + public void addLineItem_shouldThrowExceptionWhenBillIsPosted() { Bill bill = new Bill(); bill.setId(1); bill.setStatus(BillStatus.POSTED); @@ -217,9 +217,7 @@ public void addLineItem_shouldAllowAddingLineItemToExistingPostedBill() { lineItem.setPrice(BigDecimal.valueOf(100)); lineItem.setQuantity(1); - // Should not throw exception for POSTED bill bill.addLineItem(lineItem); - assertEquals(1, bill.getLineItems().size()); } @Test(expected = IllegalStateException.class) @@ -267,8 +265,8 @@ public void removeLineItem_shouldAllowRemovingLineItemFromPendingBill() { assertEquals(0, bill.getLineItems().size()); } - @Test - public void removeLineItem_shouldAllowRemovingLineItemFromPostedBill() { + @Test(expected = IllegalStateException.class) + public void removeLineItem_shouldThrowExceptionWhenBillIsPosted() { Bill bill = new Bill(); bill.setId(1); bill.setStatus(BillStatus.POSTED); @@ -279,9 +277,7 @@ public void removeLineItem_shouldAllowRemovingLineItemFromPostedBill() { lineItem.setQuantity(1); bill.getLineItems().add(lineItem); - // Should not throw exception for POSTED bill bill.removeLineItem(lineItem); - assertEquals(0, bill.getLineItems().size()); } @Test @@ -317,17 +313,15 @@ public void setLineItems_shouldAllowSettingLineItemsOnExistingPendingBill() { assertEquals(1, bill.getLineItems().size()); } - @Test - public void setLineItems_shouldAllowSettingLineItemsOnExistingPostedBill() { + @Test(expected = IllegalStateException.class) + public void setLineItems_shouldThrowExceptionWhenBillIsPosted() { Bill bill = new Bill(); bill.setId(1); bill.setStatus(BillStatus.POSTED); ArrayList existingLineItems = new ArrayList<>(); bill.setLineItems(existingLineItems); existingLineItems.add(new BillLineItem()); - // Should not throw exception for POSTED bill bill.setLineItems(existingLineItems); - assertEquals(1, bill.getLineItems().size()); } } diff --git a/api/src/test/java/org/openmrs/module/billing/impl/BillLineItemServiceImplTest.java b/api/src/test/java/org/openmrs/module/billing/impl/BillLineItemServiceImplTest.java index b809c77e..cd6e0151 100644 --- a/api/src/test/java/org/openmrs/module/billing/impl/BillLineItemServiceImplTest.java +++ b/api/src/test/java/org/openmrs/module/billing/impl/BillLineItemServiceImplTest.java @@ -73,7 +73,7 @@ public void save_shouldAllowSavingLineItemForPendingBill() { * @see org.openmrs.module.billing.api.impl.BillLineItemServiceImpl#save(BillLineItem) */ @Test - public void save_shouldAllowSavingLineItemForPostedBill() { + public void save_shouldThrowExceptionWhenSavingLineItemForPostedBill() { // Get the POSTED bill from test data (bill_id=0) Bill postedBill = billService.getById(0); assertNotNull(postedBill); @@ -83,13 +83,11 @@ public void save_shouldAllowSavingLineItemForPostedBill() { BillLineItem lineItem = postedBill.getLineItems().get(0); assertNotNull(lineItem); - // Update the line item + // Try to update the line item lineItem.setPrice(BigDecimal.valueOf(99.99)); - // Should not throw exception - BillLineItem savedItem = billLineItemService.save(lineItem); - assertNotNull(savedItem); - assertEquals(BigDecimal.valueOf(99.99), savedItem.getPrice()); + // Should throw exception + assertThrows(IllegalStateException.class, () -> billLineItemService.save(lineItem)); } /** @@ -138,7 +136,7 @@ public void voidEntity_shouldAllowVoidingLineItemForPendingBill() { * @see org.openmrs.module.billing.api.impl.BillLineItemServiceImpl#voidEntity(BillLineItem, String) */ @Test - public void voidEntity_shouldAllowVoidingLineItemForPostedBill() { + public void voidEntity_shouldThrowExceptionWhenVoidingLineItemForPostedBill() { // Get the POSTED bill from test data (bill_id=0) Bill postedBill = billService.getById(0); assertNotNull(postedBill); @@ -147,12 +145,9 @@ public void voidEntity_shouldAllowVoidingLineItemForPostedBill() { // Get a line item from the posted bill BillLineItem lineItem = postedBill.getLineItems().get(0); assertNotNull(lineItem); - assertFalse(lineItem.getVoided()); - // Should not throw exception - BillLineItem voidedItem = billLineItemService.voidEntity(lineItem, "Test void reason"); - assertNotNull(voidedItem); - assertTrue(voidedItem.getVoided()); + // Should throw exception + assertThrows(IllegalStateException.class, () -> billLineItemService.voidEntity(lineItem, "Test void reason")); } /** @@ -184,11 +179,11 @@ public void purge_shouldAllowPurgingLineItemForPendingBill() { * @see org.openmrs.module.billing.api.impl.BillLineItemServiceImpl#purge(BillLineItem) */ @Test - public void purge_shouldThrowExceptionWhenPurgingLineItemForPaidBill() { - // Get the POSTED bill from test data (bill_id=1) - Bill postedBill = billService.getById(1); + public void purge_shouldThrowExceptionWhenPurgingLineItemForPostedBill() { + // Get the POSTED bill from test data (bill_id=0) + Bill postedBill = billService.getById(0); assertNotNull(postedBill); - assertEquals(BillStatus.PAID, postedBill.getStatus()); + assertEquals(BillStatus.POSTED, postedBill.getStatus()); // Get a line item from the posted bill BillLineItem lineItem = postedBill.getLineItems().get(0); diff --git a/api/src/test/java/org/openmrs/module/billing/impl/BillServiceImplTest.java b/api/src/test/java/org/openmrs/module/billing/impl/BillServiceImplTest.java index 3a0880ba..10975392 100644 --- a/api/src/test/java/org/openmrs/module/billing/impl/BillServiceImplTest.java +++ b/api/src/test/java/org/openmrs/module/billing/impl/BillServiceImplTest.java @@ -285,24 +285,19 @@ public void save_shouldAllowAddingLineItemsToPendingBill() { * @see org.openmrs.module.billing.api.impl.BillServiceImpl#save(Bill) */ @Test - public void save_shouldAllowAddingLineItemsToPostedBill() { + public void save_shouldThrowExceptionWhenAddingLineItemsToPostedBill() { // Get the POSTED bill from test data (bill_id=0) Bill postedBill = billService.getById(0); assertNotNull(postedBill); assertEquals(BillStatus.POSTED, postedBill.getStatus()); - // Add a new line item + // Try to add a new line item BillLineItem newLineItem = new BillLineItem(); newLineItem.setPrice(BigDecimal.valueOf(25.50)); newLineItem.setQuantity(2); - newLineItem.setPaymentStatus(BillStatus.PENDING); - newLineItem.setLineItemOrder(postedBill.getLineItems().size()); - postedBill.addLineItem(newLineItem); - // Should not throw exception - Bill savedBill = billService.save(postedBill); - assertNotNull(savedBill); - assertTrue(savedBill.getLineItems().size() > 0); + // Should throw exception + assertThrows(IllegalStateException.class, () -> postedBill.addLineItem(newLineItem)); } /** @@ -351,11 +346,11 @@ public void save_shouldAllowRemovingLineItemsFromPendingBill() { * @see org.openmrs.module.billing.api.impl.BillServiceImpl#save(Bill) */ @Test - public void save_Bill_shouldThrowExceptionWhenRemovingLineItemsFromPaidBill() { - // Get the POSTED bill from test data (bill_id=1) - Bill postedBill = billService.getById(1); + public void save_shouldThrowExceptionWhenRemovingLineItemsFromPostedBill() { + // Get the POSTED bill from test data (bill_id=0) + Bill postedBill = billService.getById(0); assertNotNull(postedBill); - assertEquals(BillStatus.PAID, postedBill.getStatus()); + assertEquals(BillStatus.POSTED, postedBill.getStatus()); BillLineItem itemToRemove = postedBill.getLineItems().get(0); diff --git a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillLineItemNestedResource.java b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillLineItemNestedResource.java deleted file mode 100644 index fad6e8c8..00000000 --- a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillLineItemNestedResource.java +++ /dev/null @@ -1,162 +0,0 @@ -package org.openmrs.module.billing.web.rest.resource; - -import org.openmrs.api.context.Context; -import org.openmrs.module.billing.api.BillLineItemService; -import org.openmrs.module.billing.api.IBillService; -import org.openmrs.module.billing.api.model.Bill; -import org.openmrs.module.billing.api.model.BillLineItem; - -import org.openmrs.module.billing.api.model.BillStatus; -import org.openmrs.module.webservices.rest.web.RequestContext; -import org.openmrs.module.webservices.rest.web.annotation.PropertySetter; -import org.openmrs.module.webservices.rest.web.annotation.SubResource; -import org.openmrs.module.webservices.rest.web.representation.DefaultRepresentation; -import org.openmrs.module.webservices.rest.web.representation.FullRepresentation; -import org.openmrs.module.webservices.rest.web.representation.Representation; -import org.openmrs.module.webservices.rest.web.resource.api.PageableResult; -import org.openmrs.module.webservices.rest.web.resource.impl.AlreadyPaged; -import org.openmrs.module.webservices.rest.web.resource.impl.DelegatingResourceDescription; -import org.openmrs.module.webservices.rest.web.resource.impl.DelegatingSubResource; -import org.openmrs.module.webservices.rest.web.response.ObjectNotFoundException; -import org.openmrs.module.webservices.rest.web.response.ResponseException; - -import java.math.BigDecimal; -import java.util.ArrayList; - -@SubResource(parent = BillResource.class, path = "lineItem", supportedClass = BillLineItem.class, - supportedOpenmrsVersions = {"2.0 - 2.*"}) -public class BillLineItemNestedResource extends DelegatingSubResource { - - @Override - public DelegatingResourceDescription getRepresentationDescription(Representation rep) { - DelegatingResourceDescription description = new DelegatingResourceDescription(); - if (rep instanceof DefaultRepresentation || rep instanceof FullRepresentation) { - description.addProperty("uuid"); - description.addProperty("quantity"); - description.addProperty("price"); - description.addProperty("lineItemOrder"); - description.addProperty("paymentStatus"); - description.addProperty("item"); - description.addProperty("billableService", Representation.REF); - } - return description; - } - - @Override - public DelegatingResourceDescription getCreatableProperties() { - DelegatingResourceDescription description = new DelegatingResourceDescription(); - description.addProperty("quantity"); - description.addProperty("price"); - description.addProperty("lineItemOrder"); - description.addProperty("paymentStatus"); - return description; - } - - @Override - public BillLineItem save(BillLineItem lineItem) { - IBillService billService = Context.getService(IBillService.class); - Bill bill = lineItem.getBill(); - - // Validate bill is editable only if bill is PENDING or POSTED - if (bill == null || !bill.editable()) { - throw new IllegalArgumentException("Bill is not editable"); - } - - // Save the line item - BillLineItemService service = Context.getService(BillLineItemService.class); - return service.save(lineItem); - } - - @Override - public void delete(String parentUniqueId, final String uuid, String reason, RequestContext context) { - IBillService billService = Context.getService(IBillService.class); - Bill bill = findBill(billService, parentUniqueId); - BillLineItem lineItem = findLineItem(bill, uuid); - - // Void the line item (soft delete) - lineItem.setVoided(true); - lineItem.setVoidReason(reason); - lineItem.setVoidedBy(Context.getAuthenticatedUser()); - - // Save the bill to persist the voided status - billService.save(bill); - } - - @PropertySetter(value = "quantity") - public void setQuantity(BillLineItem instance, Integer quantity) { - instance.setQuantity(quantity); - } - - @PropertySetter(value = "price") - public void setPrice(BillLineItem instance, Object price) { - if (price instanceof Double || price instanceof Integer) { - double priceValue = ((Number) price).doubleValue(); - instance.setPrice(BigDecimal.valueOf(priceValue)); - } else { - throw new IllegalArgumentException("Unsupported price type: " + price.getClass().getName()); - } - } - - @PropertySetter(value = "lineItemOrder") - public void setLineItemOrder(BillLineItem instance, Integer order) { - instance.setLineItemOrder(order); - } - - @PropertySetter(value = "paymentStatus") - public void setPaymentStatus(BillLineItem instance, BillStatus status) { - instance.setPaymentStatus(status); - } - - @Override - public PageableResult doGetAll(Bill parent, RequestContext context) { - return new AlreadyPaged(context, - new ArrayList(parent.getLineItems()), false); - } - - @Override - public BillLineItem getByUniqueId(String uuid) { - return Context.getService(BillLineItemService.class).getByUuid(uuid); - } - - @Override - protected void delete(BillLineItem billLineItem, String s, RequestContext requestContext) throws ResponseException { - - } - - @Override - public void purge(BillLineItem billLineItem, RequestContext requestContext) throws ResponseException { - - } - - @Override - public Bill getParent(BillLineItem instance) { - return instance.getBill(); - } - - @Override - public void setParent(BillLineItem instance, Bill parent) { - instance.setBill(parent); - } - - @Override - public BillLineItem newDelegate() { - return new BillLineItem(); - } - - private Bill findBill(IBillService service, String billUUID) { - Bill bill = service.getByUuid(billUUID); - if (bill == null) { - throw new ObjectNotFoundException(); - } - return bill; - } - - private BillLineItem findLineItem(Bill bill, final String lineItemUUID) { - for (BillLineItem item : bill.getLineItems()) { - if (item != null && item.getUuid().equals(lineItemUUID)) { - return item; - } - } - throw new ObjectNotFoundException(); - } -} \ No newline at end of file diff --git a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillResource.java b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillResource.java index 3e04a5e4..e745adf0 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillResource.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillResource.java @@ -91,9 +91,9 @@ public DelegatingResourceDescription getCreatableProperties() { @PropertySetter("lineItems") public void setBillLineItems(Bill instance, List lineItems) { - if (!instance.editable()) { + if (!instance.isPending()) { throw new IllegalStateException( - "Line items can only be modified when the bill is in PENDING or POSTED state. Current status: " + "Line items can only be modified when the bill is in PENDING state. Current status: " + instance.getStatus()); } if (instance.getLineItems() == null) { From b9dacee8a387bed20be990a551406aa5f40a84b5 Mon Sep 17 00:00:00 2001 From: ganeshdevisetti Date: Sat, 27 Dec 2025 22:46:25 +0530 Subject: [PATCH 17/20] Adding Discount attributes and removal od status checks --- .../api/impl/BillLineItemServiceImpl.java | 15 +-- .../module/billing/api/model/Bill.java | 16 --- .../billing/api/model/BillLineItem.java | 34 ++++- api/src/main/resources/Bill.hbm.xml | 2 + .../module/billing/api/model/BillTest.java | 124 ++++++++++++++++++ .../rest/resource/BillLineItemResource.java | 20 +++ .../web/rest/resource/BillResource.java | 5 - omod/src/main/resources/liquibase.xml | 16 +++ 8 files changed, 193 insertions(+), 39 deletions(-) diff --git a/api/src/main/java/org/openmrs/module/billing/api/impl/BillLineItemServiceImpl.java b/api/src/main/java/org/openmrs/module/billing/api/impl/BillLineItemServiceImpl.java index 561360e2..cd83ba4f 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/impl/BillLineItemServiceImpl.java +++ b/api/src/main/java/org/openmrs/module/billing/api/impl/BillLineItemServiceImpl.java @@ -32,14 +32,7 @@ protected IEntityAuthorizationPrivileges getPrivileges() { @Override protected void validate(BillLineItem object) { - if (object != null && object.getBill() != null) { - Bill bill = object.getBill(); - if (!bill.isPending()) { - throw new IllegalStateException( - "Line items can only be modified when the bill is in PENDING state. Current status: " - + bill.getStatus()); - } - } + // Status checks removed to allow bill modification regardless of status } @Override @@ -91,12 +84,6 @@ public void purge(BillLineItem entity) { Bill bill = null; if (entity != null && entity.getBill() != null) { bill = entity.getBill(); - // Validate before purging (purge doesn't call validate()) - if (!bill.isPending()) { - throw new IllegalStateException( - "Line items can only be modified when the bill is in PENDING state. Current status: " - + bill.getStatus()); - } } super.purge(entity); diff --git a/api/src/main/java/org/openmrs/module/billing/api/model/Bill.java b/api/src/main/java/org/openmrs/module/billing/api/model/Bill.java index df2842f3..1e01bb5f 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/model/Bill.java +++ b/api/src/main/java/org/openmrs/module/billing/api/model/Bill.java @@ -182,11 +182,6 @@ public void setLineItems(List lineItems) { // Only validate if lineItems is already initialized // This prevents validation during Hibernate entity loading (when lineItems is null) // but still validates user modifications (when lineItems is already set) - if (this.lineItems != null && !isPending()) { - throw new IllegalStateException( - "Line items can only be modified when the bill is in PENDING state. Current status: " - + this.getStatus()); - } this.lineItems = lineItems; } @@ -225,12 +220,6 @@ public void addLineItem(BillLineItem item) { throw new NullPointerException("The list item to add must be defined."); } - if (!isPending()) { - throw new IllegalStateException( - "Line items can only be modified when the bill is in PENDING state. Current status: " - + this.getStatus()); - } - if (this.lineItems == null) { this.lineItems = new ArrayList(); } @@ -241,11 +230,6 @@ public void addLineItem(BillLineItem item) { public void removeLineItem(BillLineItem item) { if (item != null) { - if (!isPending()) { - throw new IllegalStateException( - "Line items can only be modified when the bill is in PENDING state. Current status: " - + this.getStatus()); - } if (this.lineItems != null) { this.lineItems.remove(item); } diff --git a/api/src/main/java/org/openmrs/module/billing/api/model/BillLineItem.java b/api/src/main/java/org/openmrs/module/billing/api/model/BillLineItem.java index 60cedc09..fe9c5752 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/model/BillLineItem.java +++ b/api/src/main/java/org/openmrs/module/billing/api/model/BillLineItem.java @@ -14,7 +14,6 @@ package org.openmrs.module.billing.api.model; import java.math.BigDecimal; -import java.util.Objects; import org.openmrs.BaseChangeableOpenmrsData; import org.openmrs.Order; @@ -51,6 +50,10 @@ public class BillLineItem extends BaseChangeableOpenmrsData { private Order order; + private BigDecimal discount; + + private String discountReason; + @Override public Integer getId() { return billLineItemId; @@ -62,12 +65,19 @@ public void setId(Integer id) { } /** - * Get the total price for the line item + * Get the total price for the line item (price * quantity - discount) * - * @return double the total price for the line item + * @return BigDecimal the total price for the line item after discount */ public BigDecimal getTotal() { - return price.multiply(BigDecimal.valueOf(quantity)); + if (price == null || quantity == null) { + return BigDecimal.ZERO; + } + BigDecimal subtotal = price.multiply(BigDecimal.valueOf(quantity)); + if (discount != null) { + return subtotal.subtract(discount); + } + return subtotal; } public CashierItemPrice getItemPrice() { @@ -149,4 +159,20 @@ public Order getOrder() { public void setOrder(Order order) { this.order = order; } + + public BigDecimal getDiscount() { + return discount; + } + + public void setDiscount(BigDecimal discount) { + this.discount = discount; + } + + public String getDiscountReason() { + return discountReason; + } + + public void setDiscountReason(String discountReason) { + this.discountReason = discountReason; + } } diff --git a/api/src/main/resources/Bill.hbm.xml b/api/src/main/resources/Bill.hbm.xml index 18560dc5..b9f850fa 100644 --- a/api/src/main/resources/Bill.hbm.xml +++ b/api/src/main/resources/Bill.hbm.xml @@ -169,6 +169,8 @@ 12 + + diff --git a/api/src/test/java/org/openmrs/module/billing/api/model/BillTest.java b/api/src/test/java/org/openmrs/module/billing/api/model/BillTest.java index f34f508a..01173926 100644 --- a/api/src/test/java/org/openmrs/module/billing/api/model/BillTest.java +++ b/api/src/test/java/org/openmrs/module/billing/api/model/BillTest.java @@ -100,6 +100,84 @@ public void getTotal_shouldReturnZeroWhenAllLineItemsAreVoided() { assertEquals(BigDecimal.ZERO, bill.getTotal()); } + @Test + public void getTotal_shouldAccountForDiscountInLineItems() { + Bill bill = new Bill(); + bill.setLineItems(new ArrayList<>()); + + BillLineItem lineItem1 = new BillLineItem(); + lineItem1.setPrice(BigDecimal.valueOf(100)); + lineItem1.setQuantity(2); + lineItem1.setDiscount(BigDecimal.valueOf(10)); + lineItem1.setVoided(false); + bill.getLineItems().add(lineItem1); + + BillLineItem lineItem2 = new BillLineItem(); + lineItem2.setPrice(BigDecimal.valueOf(50)); + lineItem2.setQuantity(1); + lineItem2.setDiscount(BigDecimal.valueOf(5)); + lineItem2.setVoided(false); + bill.getLineItems().add(lineItem2); + + // Expected: (100*2 - 10) + (50*1 - 5) = 190 + 45 = 235 + assertEquals(BigDecimal.valueOf(235), bill.getTotal()); + } + + @Test + public void getTotal_shouldHandleNullDiscount() { + Bill bill = new Bill(); + bill.setLineItems(new ArrayList<>()); + + BillLineItem lineItem1 = new BillLineItem(); + lineItem1.setPrice(BigDecimal.valueOf(100)); + lineItem1.setQuantity(2); + lineItem1.setDiscount(null); + lineItem1.setVoided(false); + bill.getLineItems().add(lineItem1); + + BillLineItem lineItem2 = new BillLineItem(); + lineItem2.setPrice(BigDecimal.valueOf(50)); + lineItem2.setQuantity(1); + lineItem2.setDiscount(BigDecimal.valueOf(5)); + lineItem2.setVoided(false); + bill.getLineItems().add(lineItem2); + + // Expected: (100*2) + (50*1 - 5) = 200 + 45 = 245 + assertEquals(BigDecimal.valueOf(245), bill.getTotal()); + } + + @Test + public void billLineItemGetTotal_shouldSubtractDiscountFromSubtotal() { + BillLineItem lineItem = new BillLineItem(); + lineItem.setPrice(BigDecimal.valueOf(100)); + lineItem.setQuantity(2); + lineItem.setDiscount(BigDecimal.valueOf(15)); + + // Expected: 100 * 2 - 15 = 185 + assertEquals(BigDecimal.valueOf(185), lineItem.getTotal()); + } + + @Test + public void billLineItemGetTotal_shouldReturnSubtotalWhenDiscountIsNull() { + BillLineItem lineItem = new BillLineItem(); + lineItem.setPrice(BigDecimal.valueOf(100)); + lineItem.setQuantity(2); + lineItem.setDiscount(null); + + // Expected: 100 * 2 = 200 + assertEquals(BigDecimal.valueOf(200), lineItem.getTotal()); + } + + @Test + public void billLineItem_shouldAllowSettingAndGettingDiscountAndDiscountReason() { + BillLineItem lineItem = new BillLineItem(); + lineItem.setDiscount(BigDecimal.valueOf(25)); + lineItem.setDiscountReason("Patient discount"); + + assertEquals(BigDecimal.valueOf(25), lineItem.getDiscount()); + assertEquals("Patient discount", lineItem.getDiscountReason()); + } + @Test public void synchronizeBillStatus_shouldUpdateStatusToPaidWhenFullyPaid() { Bill bill = new Bill(); @@ -176,6 +254,52 @@ public void synchronizeBillStatus_shouldUpdateStatusToPaidAfterVoidingLineItems( assertEquals(BillStatus.PAID, bill.getStatus()); } + @Test + public void synchronizeBillStatus_shouldUpdateStatusToPaidWhenDiscountMakesBillFullyPaid() { + Bill bill = new Bill(); + bill.setLineItems(new ArrayList<>()); + bill.setPayments(new HashSet<>()); + + BillLineItem lineItem = new BillLineItem(); + lineItem.setPrice(BigDecimal.valueOf(100)); + lineItem.setQuantity(1); + lineItem.setDiscount(BigDecimal.valueOf(20)); + lineItem.setVoided(false); + bill.getLineItems().add(lineItem); + + Payment payment = new Payment(); + payment.setAmountTendered(BigDecimal.valueOf(80)); // Total after discount is 80 + payment.setVoided(false); + bill.getPayments().add(payment); + + bill.synchronizeBillStatus(); + // Total is 100 - 20 = 80, payment is 80, so should be PAID + assertEquals(BillStatus.PAID, bill.getStatus()); + } + + @Test + public void synchronizeBillStatus_shouldUpdateStatusToPostedWhenDiscountMakesBillPartiallyPaid() { + Bill bill = new Bill(); + bill.setLineItems(new ArrayList<>()); + bill.setPayments(new HashSet<>()); + + BillLineItem lineItem = new BillLineItem(); + lineItem.setPrice(BigDecimal.valueOf(100)); + lineItem.setQuantity(1); + lineItem.setDiscount(BigDecimal.valueOf(20)); + lineItem.setVoided(false); + bill.getLineItems().add(lineItem); + + Payment payment = new Payment(); + payment.setAmountTendered(BigDecimal.valueOf(50)); // Total after discount is 80, payment is 50 + payment.setVoided(false); + bill.getPayments().add(payment); + + bill.synchronizeBillStatus(); + // Total is 100 - 20 = 80, payment is 50, so should be POSTED + assertEquals(BillStatus.POSTED, bill.getStatus()); + } + @Test public void addLineItem_shouldAllowAddingLineItemToNewBill() { Bill bill = new Bill(); diff --git a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillLineItemResource.java b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillLineItemResource.java index 42f4e73d..2c69b14a 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillLineItemResource.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillLineItemResource.java @@ -59,6 +59,8 @@ public DelegatingResourceDescription getRepresentationDescription(Representation description.addProperty("priceUuid"); description.addProperty("lineItemOrder"); description.addProperty("paymentStatus"); + description.addProperty("discount"); + description.addProperty("discountReason"); return description; } return null; @@ -120,6 +122,24 @@ public String getPriceName(BillLineItem instance) { return StringUtils.isNotBlank(itemName) ? itemName : ""; } + @PropertySetter(value = "discount") + public void setDiscount(BillLineItem instance, Object discount) { + if (discount == null) { + instance.setDiscount(null); + } else if (discount instanceof Double || discount instanceof Integer) { + double discountValue = ((Number) discount).doubleValue(); + instance.setDiscount(BigDecimal.valueOf(discountValue)); + } else { + throw new IllegalArgumentException("Unsupported discount type: " + discount.getClass().getName()); + } + } + + @PropertyGetter(value = "discountReason") + public String getDiscountReason(BillLineItem instance) { + String reason = instance.getDiscountReason(); + return StringUtils.isNotBlank(reason) ? reason : ""; + } + @PropertySetter(value = "priceUuid") public void setItemPrice(BillLineItem instance, String uuid) { StockManagementService itemDataService = Context.getService(StockManagementService.class); diff --git a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillResource.java b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillResource.java index e745adf0..48327b10 100644 --- a/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillResource.java +++ b/omod/src/main/java/org/openmrs/module/billing/web/rest/resource/BillResource.java @@ -91,11 +91,6 @@ public DelegatingResourceDescription getCreatableProperties() { @PropertySetter("lineItems") public void setBillLineItems(Bill instance, List lineItems) { - if (!instance.isPending()) { - throw new IllegalStateException( - "Line items can only be modified when the bill is in PENDING state. Current status: " - + instance.getStatus()); - } if (instance.getLineItems() == null) { instance.setLineItems(new ArrayList(lineItems.size())); } diff --git a/omod/src/main/resources/liquibase.xml b/omod/src/main/resources/liquibase.xml index 46c9152c..36c926d9 100644 --- a/omod/src/main/resources/liquibase.xml +++ b/omod/src/main/resources/liquibase.xml @@ -1003,4 +1003,20 @@ + + + + + + + Adding discount and discount_reason columns to cashier_bill_line_item table + + + + + + + + + \ No newline at end of file From e69bd07d453a7dc0616284ae6a723ce5c8493bb7 Mon Sep 17 00:00:00 2001 From: ganeshdevisetti Date: Mon, 29 Dec 2025 11:49:20 +0530 Subject: [PATCH 18/20] Test fix --- .../module/billing/api/model/BillTest.java | 68 ---------------- .../impl/BillLineItemServiceImplTest.java | 77 ------------------- .../billing/impl/BillServiceImplTest.java | 53 ------------- 3 files changed, 198 deletions(-) diff --git a/api/src/test/java/org/openmrs/module/billing/api/model/BillTest.java b/api/src/test/java/org/openmrs/module/billing/api/model/BillTest.java index 01173926..ee8a1dd0 100644 --- a/api/src/test/java/org/openmrs/module/billing/api/model/BillTest.java +++ b/api/src/test/java/org/openmrs/module/billing/api/model/BillTest.java @@ -330,48 +330,6 @@ public void addLineItem_shouldAllowAddingLineItemToExistingPendingBill() { assertEquals(1, bill.getLineItems().size()); } - @Test(expected = IllegalStateException.class) - public void addLineItem_shouldThrowExceptionWhenBillIsPosted() { - Bill bill = new Bill(); - bill.setId(1); - bill.setStatus(BillStatus.POSTED); - bill.setLineItems(new ArrayList<>()); - - BillLineItem lineItem = new BillLineItem(); - lineItem.setPrice(BigDecimal.valueOf(100)); - lineItem.setQuantity(1); - - bill.addLineItem(lineItem); - } - - @Test(expected = IllegalStateException.class) - public void addLineItem_shouldThrowExceptionWhenBillIsPaid() { - Bill bill = new Bill(); - bill.setId(1); - bill.setStatus(BillStatus.PAID); - bill.setLineItems(new ArrayList<>()); - - BillLineItem lineItem = new BillLineItem(); - lineItem.setPrice(BigDecimal.valueOf(100)); - lineItem.setQuantity(1); - - bill.addLineItem(lineItem); - } - - @Test(expected = IllegalStateException.class) - public void addLineItem_shouldThrowExceptionWhenBillIsCancelled() { - Bill bill = new Bill(); - bill.setId(1); - bill.setStatus(BillStatus.CANCELLED); - bill.setLineItems(new ArrayList<>()); - - BillLineItem lineItem = new BillLineItem(); - lineItem.setPrice(BigDecimal.valueOf(100)); - lineItem.setQuantity(1); - - bill.addLineItem(lineItem); - } - @Test public void removeLineItem_shouldAllowRemovingLineItemFromPendingBill() { Bill bill = new Bill(); @@ -389,21 +347,6 @@ public void removeLineItem_shouldAllowRemovingLineItemFromPendingBill() { assertEquals(0, bill.getLineItems().size()); } - @Test(expected = IllegalStateException.class) - public void removeLineItem_shouldThrowExceptionWhenBillIsPosted() { - Bill bill = new Bill(); - bill.setId(1); - bill.setStatus(BillStatus.POSTED); - bill.setLineItems(new ArrayList<>()); - - BillLineItem lineItem = new BillLineItem(); - lineItem.setPrice(BigDecimal.valueOf(100)); - lineItem.setQuantity(1); - bill.getLineItems().add(lineItem); - - bill.removeLineItem(lineItem); - } - @Test public void setLineItems_shouldAllowSettingLineItemsOnNewBill() { Bill bill = new Bill(); @@ -437,15 +380,4 @@ public void setLineItems_shouldAllowSettingLineItemsOnExistingPendingBill() { assertEquals(1, bill.getLineItems().size()); } - @Test(expected = IllegalStateException.class) - public void setLineItems_shouldThrowExceptionWhenBillIsPosted() { - Bill bill = new Bill(); - bill.setId(1); - bill.setStatus(BillStatus.POSTED); - ArrayList existingLineItems = new ArrayList<>(); - bill.setLineItems(existingLineItems); - existingLineItems.add(new BillLineItem()); - bill.setLineItems(existingLineItems); - } - } diff --git a/api/src/test/java/org/openmrs/module/billing/impl/BillLineItemServiceImplTest.java b/api/src/test/java/org/openmrs/module/billing/impl/BillLineItemServiceImplTest.java index cd6e0151..b72f9766 100644 --- a/api/src/test/java/org/openmrs/module/billing/impl/BillLineItemServiceImplTest.java +++ b/api/src/test/java/org/openmrs/module/billing/impl/BillLineItemServiceImplTest.java @@ -69,48 +69,6 @@ public void save_shouldAllowSavingLineItemForPendingBill() { assertEquals(BigDecimal.valueOf(99.99), savedItem.getPrice()); } - /** - * @see org.openmrs.module.billing.api.impl.BillLineItemServiceImpl#save(BillLineItem) - */ - @Test - public void save_shouldThrowExceptionWhenSavingLineItemForPostedBill() { - // Get the POSTED bill from test data (bill_id=0) - Bill postedBill = billService.getById(0); - assertNotNull(postedBill); - assertEquals(BillStatus.POSTED, postedBill.getStatus()); - - // Get a line item from the posted bill - BillLineItem lineItem = postedBill.getLineItems().get(0); - assertNotNull(lineItem); - - // Try to update the line item - lineItem.setPrice(BigDecimal.valueOf(99.99)); - - // Should throw exception - assertThrows(IllegalStateException.class, () -> billLineItemService.save(lineItem)); - } - - /** - * @see org.openmrs.module.billing.api.impl.BillLineItemServiceImpl#save(BillLineItem) - */ - @Test - public void save_shouldThrowExceptionWhenSavingLineItemForPaidBill() { - // Get the PAID bill from test data (bill_id=1) - Bill paidBill = billService.getById(1); - assertNotNull(paidBill); - assertEquals(BillStatus.PAID, paidBill.getStatus()); - - // Get a line item from the paid bill - BillLineItem lineItem = paidBill.getLineItems().get(0); - assertNotNull(lineItem); - - // Try to update the line item - lineItem.setPrice(BigDecimal.valueOf(99.99)); - - // Should throw exception - assertThrows(IllegalStateException.class, () -> billLineItemService.save(lineItem)); - } - /** * @see org.openmrs.module.billing.api.impl.BillLineItemServiceImpl#voidEntity(BillLineItem, String) */ @@ -132,24 +90,6 @@ public void voidEntity_shouldAllowVoidingLineItemForPendingBill() { assertTrue(voidedItem.getVoided()); } - /** - * @see org.openmrs.module.billing.api.impl.BillLineItemServiceImpl#voidEntity(BillLineItem, String) - */ - @Test - public void voidEntity_shouldThrowExceptionWhenVoidingLineItemForPostedBill() { - // Get the POSTED bill from test data (bill_id=0) - Bill postedBill = billService.getById(0); - assertNotNull(postedBill); - assertEquals(BillStatus.POSTED, postedBill.getStatus()); - - // Get a line item from the posted bill - BillLineItem lineItem = postedBill.getLineItems().get(0); - assertNotNull(lineItem); - - // Should throw exception - assertThrows(IllegalStateException.class, () -> billLineItemService.voidEntity(lineItem, "Test void reason")); - } - /** * @see org.openmrs.module.billing.api.impl.BillLineItemServiceImpl#purge(BillLineItem) */ @@ -175,21 +115,4 @@ public void purge_shouldAllowPurgingLineItemForPendingBill() { assertTrue(updatedBill.getLineItems().size() < originalSize); } - /** - * @see org.openmrs.module.billing.api.impl.BillLineItemServiceImpl#purge(BillLineItem) - */ - @Test - public void purge_shouldThrowExceptionWhenPurgingLineItemForPostedBill() { - // Get the POSTED bill from test data (bill_id=0) - Bill postedBill = billService.getById(0); - assertNotNull(postedBill); - assertEquals(BillStatus.POSTED, postedBill.getStatus()); - - // Get a line item from the posted bill - BillLineItem lineItem = postedBill.getLineItems().get(0); - assertNotNull(lineItem); - - // Should throw exception - assertThrows(IllegalStateException.class, () -> billLineItemService.purge(lineItem)); - } } diff --git a/api/src/test/java/org/openmrs/module/billing/impl/BillServiceImplTest.java b/api/src/test/java/org/openmrs/module/billing/impl/BillServiceImplTest.java index 10975392..9dba9298 100644 --- a/api/src/test/java/org/openmrs/module/billing/impl/BillServiceImplTest.java +++ b/api/src/test/java/org/openmrs/module/billing/impl/BillServiceImplTest.java @@ -281,44 +281,6 @@ public void save_shouldAllowAddingLineItemsToPendingBill() { assertTrue(savedBill.getLineItems().size() > 0); } - /** - * @see org.openmrs.module.billing.api.impl.BillServiceImpl#save(Bill) - */ - @Test - public void save_shouldThrowExceptionWhenAddingLineItemsToPostedBill() { - // Get the POSTED bill from test data (bill_id=0) - Bill postedBill = billService.getById(0); - assertNotNull(postedBill); - assertEquals(BillStatus.POSTED, postedBill.getStatus()); - - // Try to add a new line item - BillLineItem newLineItem = new BillLineItem(); - newLineItem.setPrice(BigDecimal.valueOf(25.50)); - newLineItem.setQuantity(2); - - // Should throw exception - assertThrows(IllegalStateException.class, () -> postedBill.addLineItem(newLineItem)); - } - - /** - * @see org.openmrs.module.billing.api.impl.BillServiceImpl#save(Bill) - */ - @Test - public void save_shouldThrowExceptionWhenAddingLineItemsToPaidBill() { - // Get the PAID bill from test data (bill_id=1) - Bill paidBill = billService.getById(1); - assertNotNull(paidBill); - assertEquals(BillStatus.PAID, paidBill.getStatus()); - - // Try to add a new line item - BillLineItem newLineItem = new BillLineItem(); - newLineItem.setPrice(BigDecimal.valueOf(25.50)); - newLineItem.setQuantity(2); - - // Should throw exception - assertThrows(IllegalStateException.class, () -> paidBill.addLineItem(newLineItem)); - } - /** * @see org.openmrs.module.billing.api.impl.BillServiceImpl#save(Bill) */ @@ -342,19 +304,4 @@ public void save_shouldAllowRemovingLineItemsFromPendingBill() { assertTrue(savedBill.getLineItems().size() < originalSize); } - /** - * @see org.openmrs.module.billing.api.impl.BillServiceImpl#save(Bill) - */ - @Test - public void save_shouldThrowExceptionWhenRemovingLineItemsFromPostedBill() { - // Get the POSTED bill from test data (bill_id=0) - Bill postedBill = billService.getById(0); - assertNotNull(postedBill); - assertEquals(BillStatus.POSTED, postedBill.getStatus()); - - BillLineItem itemToRemove = postedBill.getLineItems().get(0); - - // Should throw exception - assertThrows(IllegalStateException.class, () -> postedBill.removeLineItem(itemToRemove)); - } } From 53038265321905015c542203fa75f04417a55159 Mon Sep 17 00:00:00 2001 From: Mutagubya Jonathan Date: Fri, 23 Jan 2026 12:44:42 +0300 Subject: [PATCH 19/20] allow edit bill,payment using the BillUuid (#14) --- .../billing/api/impl/BillServiceImpl.java | 72 +++++++++---------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/api/src/main/java/org/openmrs/module/billing/api/impl/BillServiceImpl.java b/api/src/main/java/org/openmrs/module/billing/api/impl/BillServiceImpl.java index d8359d58..19046c2b 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/impl/BillServiceImpl.java +++ b/api/src/main/java/org/openmrs/module/billing/api/impl/BillServiceImpl.java @@ -135,45 +135,45 @@ public Bill save(Bill bill) { } } - // force new bill save without merging when forceNewBill is set true - if (bill.getForceNewBill() != null && bill.getForceNewBill()) { - // Skip merge logic, just save the bill as new - return super.save(bill); - } - - // Check if there is an existing pending bill for the patient - List bills = searchBill(bill.getPatient()); - if (!bills.isEmpty()) { - Bill billToUpdate = bills.get(0); - billToUpdate.setStatus(BillStatus.PENDING); - - // Handle the case where bill and billToUpdate are the same object reference - // (Hibernate session cache returns same managed instance) - Set existingItemsSet = new HashSet<>(billToUpdate.getLineItems()); - - for (BillLineItem item : bill.getLineItems()) { - // Only add if not already present (BillLineItem.equals() handles comparison) - if (!existingItemsSet.contains(item)) { - item.setBill(billToUpdate); - billToUpdate.getLineItems().add(item); - } - } - - // Calculate the total payments made on the bill (excluding voided payments) - BigDecimal totalPaid = billToUpdate.getTotalPayments(); - - // Check if the bill is fully paid - if (totalPaid.compareTo(billToUpdate.getTotal()) >= 0) { - billToUpdate.setStatus(BillStatus.PAID); - } else { + // If bill has an ID or UUID, this is an UPDATE (e.g., POST /bill/{uuid}) + // If forceNewBill is true, always create a new bill + boolean isUpdate = bill.getId() != null || StringUtils.isNotBlank(bill.getUuid()); + boolean forceNew = Boolean.TRUE.equals(bill.getForceNewBill()); + + if (!isUpdate && !forceNew) { + // Check if there is an existing pending bill for the patient + List bills = searchBill(bill.getPatient()); + if (!bills.isEmpty()) { + Bill billToUpdate = bills.get(0); billToUpdate.setStatus(BillStatus.PENDING); + + // Handle the case where bill and billToUpdate are the same object reference + // (Hibernate session cache returns same managed instance) + Set existingItemsSet = new HashSet<>(billToUpdate.getLineItems()); + + for (BillLineItem item : bill.getLineItems()) { + // Only add if not already present (BillLineItem.equals() handles comparison) + if (!existingItemsSet.contains(item)) { + item.setBill(billToUpdate); + billToUpdate.getLineItems().add(item); + } + } + + // Calculate the total payments made on the bill (excluding voided payments) + BigDecimal totalPaid = billToUpdate.getTotalPayments(); + + // Check if the bill is fully paid + if (totalPaid.compareTo(billToUpdate.getTotal()) >= 0) { + billToUpdate.setStatus(BillStatus.PAID); + } else { + billToUpdate.setStatus(BillStatus.PENDING); + } + + // Save the updated bill + return super.save(billToUpdate); } - - // Save the updated bill - return super.save(billToUpdate); } - - // If no pending bill exists, just save the new bill as it is + // For updates or forceNewBill, skip merging and save as-is return super.save(bill); } From 388e50339424719c601fb585c7af4022c20dea1e Mon Sep 17 00:00:00 2001 From: Senthil Athiban Date: Mon, 3 Aug 2026 22:09:00 +0530 Subject: [PATCH 20/20] feat: add_gh_publisher --- pom.xml | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index d815d5be..88dbf225 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ Module to provide basic billing functionality https://github.com/openmrs/openmrs-module-billing - + + + + + github + GitHub Packages + https://maven.pkg.github.com/indiemr/openmrs-module-billing + + + github + GitHub Packages + https://maven.pkg.github.com/indiemr/openmrs-module-billing + +