Skip to content
Open
6 changes: 6 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ concurrency:
jobs:
build:
uses: openmrs/openmrs-contrib-gha-workflows/.github/workflows/build-backend-module.yml@main
# Java 8 is excluded because the querystore-api dependency is compiled with Java 11
# bytecode (class version 55). A Java 8 deployment cannot load it at runtime, so the
# Java 8 matrix cell would only ever surface a compatibility error that doesn't apply
# to any deployment that can actually use this slice.
with:
java_versions: '[11, 17, 21]'
permissions:
contents: read
id-token: write
Expand Down
5 changes: 5 additions & 0 deletions api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@
<artifactId>event-api</artifactId>
</dependency>

<dependency>
<groupId>org.openmrs.module</groupId>
<artifactId>querystore-api</artifactId>
</dependency>

<dependency>
<groupId>org.openmrs.module</groupId>
<artifactId>uiframework-api</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,22 @@
*/
package org.openmrs.module.billing.api.impl;

import java.util.Date;
import java.util.List;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.openmrs.api.context.Context;
import org.openmrs.module.billing.api.BillDiscountService;
import org.openmrs.module.billing.api.BillService;
import org.openmrs.module.billing.api.db.BillDiscountDAO;
import org.openmrs.module.billing.api.model.Bill;
import org.openmrs.module.billing.api.model.BillDiscount;
import org.openmrs.module.billing.api.model.DiscountStatus;
import org.openmrs.module.billing.api.util.PrivilegeConstants;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@RequiredArgsConstructor
public class BillDiscountServiceImpl implements BillDiscountService {

Expand Down Expand Up @@ -56,12 +63,42 @@ public List<BillDiscount> getDiscountsByBillId(Integer billId) {
@Override
@Transactional
public BillDiscount saveBillDiscount(BillDiscount billDiscount) {
return billDiscountDAO.saveBillDiscount(billDiscount);
BillDiscount saved = billDiscountDAO.saveBillDiscount(billDiscount);
touchParentBill(saved);
return saved;
}

@Override
@Transactional(readOnly = true)
public DiscountStatus getStatusById(Integer id) {
return billDiscountDAO.getStatusById(id);
}

// Bill.getAmountAfterDiscount() is derived; a discount mutation changes the bill's effective
// value without touching any bill column, so the parent row stays clean. Re-save to advance
// dateChanged — the querystore BillIndexingAdvice fires on the resulting BillService.saveBill.
private void touchParentBill(BillDiscount discount) {
Integer billId = discount.getBill() == null ? null : discount.getBill().getId();
if (billId == null) {
log.error("Saved discount {} has no associated bill; skipping parent bill touch", discount.getUuid());
return;
}
try {
Context.addProxyPrivilege(PrivilegeConstants.MANAGE_BILLS);
Bill freshBill = Context.getService(BillService.class).getBill(billId);
if (freshBill == null) {
// Bill was concurrently voided/purged between this discount's save and the reload —
// recoverable race, not a hard failure. warn rather than error so ops dashboards
// don't page on routine concurrent edits.
log.warn("Discount {} references bill {} which could not be loaded; parent bill not touched",
discount.getUuid(), billId);
return;
}
freshBill.setDateChanged(new Date());
Context.getService(BillService.class).saveBill(freshBill);
}
finally {
Context.removeProxyPrivilege(PrivilegeConstants.MANAGE_BILLS);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,21 @@
package org.openmrs.module.billing.api.impl;

import java.util.Collections;
import java.util.Date;
import java.util.List;

import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.openmrs.Order;
import org.openmrs.api.context.Context;
import org.openmrs.api.impl.BaseOpenmrsService;
import org.openmrs.module.billing.api.BillLineItemService;
import org.openmrs.module.billing.api.BillService;
import org.openmrs.module.billing.api.db.BillLineItemDAO;
import org.openmrs.module.billing.api.model.Bill;
import org.openmrs.module.billing.api.model.BillLineItem;
import org.openmrs.module.billing.api.util.PrivilegeConstants;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
Expand Down Expand Up @@ -62,5 +67,32 @@ public void voidBillLineItem(BillLineItem lineItem, String voidReason) {
throw new IllegalArgumentException("voidReason cannot be null or empty");
}
billLineItemDAO.saveBillLineItem(lineItem);
touchParentBill(lineItem);
}

// Bill.getTotal() / getAmountAfterDiscount() / getTotalPayments() all skip voided line items,
// so voiding a line item changes the bill's effective value without touching any bill column.
// Re-save the parent so dateChanged advances and the querystore BillIndexingAdvice fires on
// the resulting BillService.saveBill.
private void touchParentBill(BillLineItem lineItem) {
Integer billId = lineItem.getBill() == null ? null : lineItem.getBill().getId();
if (billId == null) {
log.error("Voided line item {} has no associated bill; skipping parent bill touch", lineItem.getUuid());
return;
}
try {
Context.addProxyPrivilege(PrivilegeConstants.MANAGE_BILLS);
Bill freshBill = Context.getService(BillService.class).getBill(billId);
if (freshBill == null) {
log.warn("Line item {} references bill {} which could not be loaded; parent bill not touched",
lineItem.getUuid(), billId);
return;
}
freshBill.setDateChanged(new Date());
Context.getService(BillService.class).saveBill(freshBill);
}
finally {
Context.removeProxyPrivilege(PrivilegeConstants.MANAGE_BILLS);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
* the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
*
* Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
* graphic logo is a trademark of OpenMRS Inc.
*/
package org.openmrs.module.billing.api.querystore;

import java.util.Collections;
import java.util.Set;

import org.openmrs.api.context.Context;
import org.openmrs.module.billing.api.model.BillDiscount;
import org.openmrs.module.querystore.bridge.AbstractIndexingAdvice;

public class BillDiscountIndexingAdvice extends AbstractIndexingAdvice<BillDiscount> {

// BillDiscountService exposes only saveBillDiscount — approve / reject / void all happen by
// mutating the entity and calling saveBillDiscount, routed through AbstractIndexingAdvice's
// per-node voided policy (voided rows go to delete on the resave path). No purge method
// exists, so PURGE_METHODS is empty rather than aspirationally listing a name AOP can never
// match — see IndexingAdviceConfigTest.
static final Set<String> TRIGGER_METHODS = Collections.singleton("saveBillDiscount");

static final Set<String> PURGE_METHODS = Collections.emptySet();

@Override
protected Class<BillDiscount> getSupportedType() {
return BillDiscount.class;
}

@Override
protected BillDiscountRecordSerializer serializer() {
return Context.getRegisteredComponent("billing.querystore.serializer.bill_discount",
BillDiscountRecordSerializer.class);
}

@Override
protected Set<String> triggerMethods() {
return TRIGGER_METHODS;
}

@Override
protected Set<String> purgeMethods() {
return PURGE_METHODS;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
* the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
*
* Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
* graphic logo is a trademark of OpenMRS Inc.
*/
package org.openmrs.module.billing.api.querystore;

import java.math.BigDecimal;
import java.time.LocalDate;

import org.openmrs.Patient;
import org.openmrs.module.billing.api.model.Bill;
import org.openmrs.module.billing.api.model.BillDiscount;
import org.openmrs.module.billing.api.model.BillLineItem;
import org.openmrs.module.billing.api.model.DiscountStatus;
import org.openmrs.module.billing.api.model.DiscountType;
import org.openmrs.module.querystore.model.QueryDocument;
import org.openmrs.module.querystore.serialization.AbstractRecordSerializer;
import org.openmrs.module.querystore.util.DateFormatUtil;

public class BillDiscountRecordSerializer extends AbstractRecordSerializer<BillDiscount> {

@Override
public String getResourceType() {
return BillingQueryStoreConstants.RESOURCE_TYPE_BILL_DISCOUNT;
}

@Override
public Class<BillDiscount> getSupportedType() {
return BillDiscount.class;
}

@Override
protected String getPatientUuid(BillDiscount discount) {
Bill bill = discount.getBill();
if (bill == null) {
return null;
}
Patient patient = bill.getPatient();
return patient != null ? patient.getUuid() : null;
}

@Override
protected String getResourceUuid(BillDiscount discount) {
return discount.getUuid();
}

@Override
protected LocalDate getDate(BillDiscount discount) {
return DateFormatUtil.toLocalDate(discount.getDateCreated());
}

@Override
protected void populate(BillDiscount discount, QueryDocument doc) {
Bill bill = discount.getBill();
if (bill == null || bill.getPatient() == null) {
return;
}
// Defensive: if discountType or discountValue is null the validator should have rejected
// the row, but the indexing advice swallows RuntimeException per-entity and would silently
// drop the discount from the approval queue. Skip the document rather than NPE inside
// getDiscountAmount().
if (discount.getDiscountType() == null || discount.getDiscountValue() == null) {
return;
}

DiscountStatus status = discount.getStatus();
DiscountType type = discount.getDiscountType();
BigDecimal value = discount.getDiscountValue();
BigDecimal amount = discount.getDiscountAmount();
String receiptOrUuid = bill.getReceiptNumber() != null ? bill.getReceiptNumber() : bill.getUuid();

doc.setText(String.format("Discount on bill %s. Status: %s. Type: %s. Value: %s. Amount: %s. Reason: %s.",
receiptOrUuid, status != null ? status.name() : "UNKNOWN", type.name(), value.toPlainString(),
amount.toPlainString(), discount.getJustification() != null ? discount.getJustification() : ""));

doc.putMetadata(BillingQueryStoreConstants.FIELD_BILL_UUID, bill.getUuid());
doc.putMetadata(BillingQueryStoreConstants.FIELD_RECEIPT_NUMBER, bill.getReceiptNumber());
doc.putMetadata(BillingQueryStoreConstants.FIELD_STATUS, status != null ? status.name() : null);
doc.putMetadata(BillingQueryStoreConstants.FIELD_DISCOUNT_TYPE, type.name());
doc.putMetadata(BillingQueryStoreConstants.FIELD_DISCOUNT_VALUE, value);
doc.putMetadata(BillingQueryStoreConstants.FIELD_DISCOUNT_AMOUNT, amount);
doc.putMetadata(BillingQueryStoreConstants.FIELD_JUSTIFICATION, discount.getJustification());
doc.putMetadata(BillingQueryStoreConstants.FIELD_VOIDED, discount.getVoided());

BillLineItem lineItem = discount.getLineItem();
if (lineItem != null) {
doc.putMetadata(BillingQueryStoreConstants.FIELD_BILL_LINE_ITEM_UUID, lineItem.getUuid());
}
if (discount.getInitiator() != null) {
doc.putMetadata(BillingQueryStoreConstants.FIELD_INITIATOR_UUID, discount.getInitiator().getUuid());
}
if (discount.getApprover() != null) {
doc.putMetadata(BillingQueryStoreConstants.FIELD_APPROVER_UUID, discount.getApprover().getUuid());
}

BillingAuditFields.populate(doc, discount);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
* the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
*
* Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
* graphic logo is a trademark of OpenMRS Inc.
*/
package org.openmrs.module.billing.api.querystore;

import org.openmrs.module.querystore.bootstrap.TypeBootstrapper;
import org.openmrs.module.querystore.serialization.ClinicalRecordSerializer;
import org.openmrs.module.querystore.spi.ResourceTypeProvider;

public class BillDiscountResourceTypeProvider implements ResourceTypeProvider {

private final BillDiscountRecordSerializer serializer;

public BillDiscountResourceTypeProvider(BillDiscountRecordSerializer serializer) {
this.serializer = serializer;
}

@Override
public String getResourceType() {
return BillingQueryStoreConstants.RESOURCE_TYPE_BILL_DISCOUNT;
}

@Override
public ClinicalRecordSerializer<?> getSerializer() {
return serializer;
}

@Override
public TypeBootstrapper<?> getBootstrapper() {
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
* the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
*
* Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
* graphic logo is a trademark of OpenMRS Inc.
*/
package org.openmrs.module.billing.api.querystore;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import org.openmrs.api.context.Context;
import org.openmrs.module.billing.api.model.Bill;
import org.openmrs.module.querystore.bridge.AbstractIndexingAdvice;

public class BillIndexingAdvice extends AbstractIndexingAdvice<Bill> {

static final Set<String> TRIGGER_METHODS = new HashSet<>(
Arrays.asList("saveBill", "voidBill", "unvoidBill", "purgeBill"));

static final Set<String> PURGE_METHODS = Collections.singleton("purgeBill");

@Override
protected Class<Bill> getSupportedType() {
return Bill.class;
}

@Override
protected BillRecordSerializer serializer() {
return Context.getRegisteredComponent("billing.querystore.serializer.bill", BillRecordSerializer.class);
}

@Override
protected Set<String> triggerMethods() {
return TRIGGER_METHODS;
}

@Override
protected Set<String> purgeMethods() {
return PURGE_METHODS;
}
}
Loading