Skip to content

O3-5370: Auto-Generate Billable Items from Medication Orders#156

Open
wikumChamith wants to merge 2 commits intoopenmrs:mainfrom
wikumChamith:O3-5370
Open

O3-5370: Auto-Generate Billable Items from Medication Orders#156
wikumChamith wants to merge 2 commits intoopenmrs:mainfrom
wikumChamith:O3-5370

Conversation

@wikumChamith
Copy link
Member

@wikumChamith wikumChamith commented Mar 25, 2026

  • Replace broken AOP interceptors (GenerateBillFromOrderAdvice, OrderCreationMethodBeforeAdvice) with OpenMRS Events module for order billing
  • Subscribe to Order.CREATED events to auto-generate bills when orders are saved
  • Implement Strategy pattern (OrderBillingStrategy) for per-order-type billing rules
  • Handle NEW, REVISE (void old + create new), and DISCONTINUE (void old) order actions
  • Add BillingEventListener interface for auto-discovery new listeners only need a Spring bean, no activator changes
  • Fix BillLineItem.order HBM mapping so order-to-line-item link actually persists
  • Add getBillLineItemByOrder() to DAO/service layer for looking up line items by order

Ticket: https://openmrs.atlassian.net/browse/O3-5372?focusedCommentId=191765

@codecov-commenter
Copy link

Codecov Report

❌ Patch coverage is 36.66667% with 133 lines in your changes missing coverage. Please review.
✅ Project coverage is 28.71%. Comparing base (1778231) to head (9fd2bf0).

Files with missing lines Patch % Lines
...api/billing/impl/AbstractOrderBillingStrategy.java 49.36% 34 Missing and 6 partials ⚠️
...ing/api/billing/impl/DrugOrderBillingStrategy.java 6.06% 31 Missing ⚠️
...billing/api/billing/OrderBillingEventListener.java 15.15% 26 Missing and 2 partials ⚠️
...openmrs/module/billing/BillingModuleActivator.java 0.00% 26 Missing ⚠️
...ing/api/billing/impl/TestOrderBillingStrategy.java 81.25% 2 Missing and 4 partials ⚠️
...dule/billing/api/impl/BillLineItemServiceImpl.java 33.33% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main     #156      +/-   ##
============================================
+ Coverage     26.73%   28.71%   +1.98%     
- Complexity      487      520      +33     
============================================
  Files           190      192       +2     
  Lines          4309     4328      +19     
  Branches        486      471      -15     
============================================
+ Hits           1152     1243      +91     
+ Misses         3056     2972      -84     
- Partials        101      113      +12     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

try {
switch (order.getAction()) {
case NEW:
return handleNewOrder(order);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neither handleNewOrder() in the DrugOrderBillingStrategy and TestOrderBillingStrategy implementation checks if a BillLineItem already exists for the order. Its mentioned in the interface Javadoc that implementations should do that -

* Generate and persist a bill for the given order. Implementations should check for duplicates
* (idempotency) before creating a new bill.

}
}
catch (Exception e) {
log.error("Error processing order (action={}): {}", order.getAction(), e.getMessage(), e);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sort of nit, but doesn’t this make it hard to catch errors because anything even a deliberate "no billable service configured" all would produce the exact same output? Should we let unexpected exceptions propagate or be caught and rethrown as a specific runtime exception with actionable context (order UUID, action, strategy name)?


for (OrderBillingStrategy strategy : strategies) {
if (strategy.supports(order)) {
strategy.generateBill(order);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

strategy.generateBill(order) is called but the Optional<Bill> result is never inspected. There is no log line for a successful bill creation and no way to distinguish a deliberate skip from a silent failure. Should we log the outcome — bill.getUuid() on success, or a named reason on empty?

existingLineItem.setVoided(true);
existingLineItem.setVoidReason(reason);
existingLineItem.setDateVoided(new Date());
existingLineItem.setVoidedBy(Context.getAuthenticatedUser());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since setVoidedBy(Context.getAuthenticatedUser()) runs inside Daemon.runInDaemonThread(), wouldn’t the authenticated user get set as the daemon?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add some tests for this?

protected abstract Optional<Bill> handleNewOrder(Order order);

private Optional<Bill> handleRevisedOrder(Order order) {
voidPreviousLineItem(order, "Order revised");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

voidPreviousLineItem and handleNewOrder are separate service calls with no transactional wrapper (daemon thread,no @Transactional). If handleNewOrder fails after voidPreviousLineItem succeeds, the old bill is voided with no replacement.

Comment on lines +22 to +26
* Strategy for generating a bill from an order. Implementations are Spring beans registered by
* name. To override the default behavior for a given order type, register a bean with the same name
* (e.g. "drugOrderBillingStrategy") and mark it {@code @Primary}, or register a custom bean and
* give it a higher {@code @org.springframework.core.annotation.Order} priority.
*/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does processOrder() sort by @order. Does it just iterate whatever Spring returns and does @Primary affect getRegisteredComponents()?

DrugOrder drugOrder = (DrugOrder) order;

StockManagementService stockService = Context.getService(StockManagementService.class);
Integer drugId = drugOrder.getDrug() != null ? drugOrder.getDrug().getDrugId() : 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might have been what had been there till now, but do we really want to set the drug id to zero if the drug id is null?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants