Discussion: Bill Line Item Validation, Pending Bill Merging & Payment Behaviour

Hi all,

I’m starting this thread to explain the billing changes recently implemented, why they were required, and to get feedback on a particular behaviour in the billing flow.


Why this change was required

A validation was added earlier to prevent modifications once a bill becomes POSTED.

However, this resulted in a major side effect:

:right_arrow: Users could no longer make payments on POSTED bills, even though payments do not alter bill line items.

On investigation, the root issue was not the rule itself, but where the validation logic was placed.


Compact Summary of the Implemented Solution

Primary Goal:

Ensure that bill line items can only be modified when a bill is PENDING, while still allowing valid actions like payments on POSTED bills.

Key Implementation Updates

Area What Was Done Why
BillLineItem.equals() / hashCode() Implemented business-based equality using UUID, quantity, price, item UUID, billable service UUID, voided flag Detect duplicates and identify real changes to line items
BillResource.setBillLineItems() Validation now checks both bill status and whether line items actually changed Prevents blocking legitimate payments while still enforcing immutability of line items on non-PENDING bills

Business Rules Enforced

:check_mark: Line items editable only when bill is PENDING/new

:check_mark: Duplicate line items prevented when merging pending bills

:check_mark: Payments allowed even on POSTED bills if line items are unchanged

:check_mark: REST layer ensures immutability once status moves past PENDING


Key Technical Findings

Unexpected mutation before setBillLineItems() runs

While debugging, I found that before the setBillLineItems() method in BillResource is invoked, the Bill instance already contains modified line items.

Cause:

  • ConversionUtil.convertMap() mutates the line item collection directly on the existing Bill instance instead of preparing a detached copy.

This means by the time setBillLineItems() executes, both the original and the incoming line items appear identical, making comparison unreliable.

Workaround Implemented

To correctly validate changes:

  • I had to evict the current bill instance from the Hibernate session,
  • Reload the original bill from the database, and
  • Compare line items against this clean version before allowing or rejecting modifications.

This ensures we detect real user-initiated changes despite Hibernate and ConversionUtil mutating collections earlier in the flow.


Question for the community

Is there a better design approach for handling this validation, given the following constraints?

  • Hibernate mutates entity collections before resource setters are called
  • ConversionUtil writes directly into domain objects
  • We need a reliable pre-mutation comparison of line items to enforce the “immutability after POSTED” rule

Any architectural guidance or examples from other OpenMRS modules that handle similar REST + Hibernate flows would be greatly appreciated.


PR: O3-5246: Fixing the payments issue Post addition of Pending state check by jayg2002 · Pull Request #70 · openmrs/openmrs-module-billing · GitHub

Some Pictures that might help paint some picture:

Location domain object ref is being taken:

@dkigen @dkayiwa @ibacher @wikumc @nethmi

Our pattern has been creating validators for these objects. In this case, you would have something like a BillValidator. Did you evaluate that option? Here is a list of such validators that you can look at: openmrs-core/api/src/main/java/org/openmrs/validator at master · openmrs/openmrs-core · GitHub

I had looked into that, i have seen that validation is applied just before saving , in the billing module there are no validators but just a validate function in BillServiceImpl.java

I believe i can move the logic here, or is it recommended to create a validator file ?

Have you tried to look at the sample validator list that i sent above and followed the same pattern?

I saw the pattern and , issue is in this current Architecture :

  • validate() method in BillServiceImpl (throws exceptions)

  • Called directly from BaseObjectDataServiceImpl.save()

  • Exception-based validation (fail-fast)

Challenges:

  • The current validate() method is called directly and throws exceptions

  • Spring Validators use Errors objects (different pattern)

  • Would need to bridge between the two approaches

  • The base class doesn’t currently integrate with Spring Validators

Implementing the above requires lot or architectural changes that i believe is not recommended in this scenario

I have tried using the validate function from the BaseObjectDataServiceImpl too , the current architecture its not possible as the current scenario has the Original data already being persisted

Evidence:

One more problem:

While modifying the bill line items one cannot let the propertysetter for the setBillStatus as that causes an issue as the rounding item is not specified during a billLineItem modification:

{
    "error": {
        "message": "[status on class org.openmrs.module.billing.api.model.Bill => No rounding item specified in options. This must be set in order to use rounding for bill totals.]",
        "code": "org.openmrs.module.webservices.rest.web.resource.impl.BaseDelegatingResource:735",
        "detail": "org.openmrs.module.webservices.rest.web.response.ConversionException: status on class org.openmrs.module.billing.api.model.Bill\n\tat org.openmrs.module.webservices.rest.web.resource.impl.BaseDelegatingResource.setProperty(BaseDelegatingResource.java:735)\n\tat org.openmrs.module.webservices.rest.web.resource.impl.BaseDelegatingResource.setConvertedProperties(BaseDelegatingResource.java:606)\n\tat org.openmrs.module.webservices.rest.web.resource.impl.DelegatingCrudResource.update(DelegatingCrudResource.java:143)\n\tat org.openmrs.module.webservices.rest.web.v1_0.controller.MainResourceController.update(MainResourceController.java:134)\n\tat sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\n\tat sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)\n\tat sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\n\tat java.lang.reflect.Method.invoke(Method.java:498)\n\tat org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205)\n\tat org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:150)\n\tat org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117)\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895)\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808)\n\tat org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)\n\tat org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1072)\n\tat org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:965)\n\tat org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)\n\tat org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909)\n\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:681)\n\tat org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)\n\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:764)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.openmrs.module.web.filter.ForcePasswordChangeFilter.doFilter(ForcePasswordChangeFilter.java:61)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.openmrs.module.web.filter.ModuleFilterChain.doFilter(ModuleFilterChain.java:73)\n\tat org.openmrs.web.xss.XSSFilter.doFilter(XSSFilter.java:39)\n\tat org.openmrs.module.web.filter.ModuleFilterChain.doFilter(ModuleFilterChain.java:71)\n\tat org.openmrs.web.filter.GZIPFilter.doFilterInternal(GZIPFilter.java:66)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)\n\tat org.openmrs.module.web.filter.ModuleFilterChain.doFilter(ModuleFilterChain.java:71)\n\tat org.openmrs.module.webservices.rest.web.filter.AuthorizationFilter.doFilter(AuthorizationFilter.java:121)\n\tat org.openmrs.module.web.filter.ModuleFilterChain.doFilter(ModuleFilterChain.java:71)\n\tat org.openmrs.module.webservices.rest.web.filter.ContentTypeFilter.doFilter(ContentTypeFilter.java:64)\n\tat org.openmrs.module.web.filter.ModuleFilterChain.doFilter(ModuleFilterChain.java:71)\n\tat org.springframework.web.filter.ShallowEtagHeaderFilter.doFilterInternal(ShallowEtagHeaderFilter.java:106)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)\n\tat org.openmrs.module.web.filter.ModuleFilterChain.doFilter(ModuleFilterChain.java:71)\n\tat org.openmrs.module.authentication.web.ForcePasswordChangeFilter.doFilter(ForcePasswordChangeFilter.java:68)\n\tat org.openmrs.module.web.filter.ModuleFilterChain.doFilter(ModuleFilterChain.java:71)\n\tat org.openmrs.module.authentication.web.AuthenticationFilter.doFilter(AuthenticationFilter.java:165)\n\tat org.openmrs.module.web.filter.ModuleFilterChain.doFilter(ModuleFilterChain.java:71)\n\tat org.openmrs.module.web.filter.ModuleFilter.doFilter(ModuleFilter.java:57)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.owasp.csrfguard.CsrfGuardFilter.handleSession(CsrfGuardFilter.java:107)\n\tat org.owasp.csrfguard.CsrfGuardFilter.doFilter(CsrfGuardFilter.java:97)\n\tat org.owasp.csrfguard.CsrfGuardFilter.doFilter(CsrfGuardFilter.java:68)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.openmrs.web.filter.OpenmrsFilter.doFilterInternal(OpenmrsFilter.java:114)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.openmrs.web.filter.CookieClearingFilter.doFilterInternal(CookieClearingFilter.java:77)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.springframework.orm.hibernate5.support.OpenSessionInViewFilter.doFilterInternal(OpenSessionInViewFilter.java:156)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.springframework.web.multipart.support.MultipartFilter.doFilterInternal(MultipartFilter.java:125)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.openmrs.web.filter.StartupFilter.doFilter(StartupFilter.java:120)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.openmrs.web.filter.StartupFilter.doFilter(StartupFilter.java:120)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.openmrs.web.filter.StartupFilter.doFilter(StartupFilter.java:120)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:197)\n\tat org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)\n\tat org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542)\n\tat org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:135)\n\tat org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)\n\tat org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)\n\tat org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357)\n\tat org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:382)\n\tat org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)\n\tat org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:893)\n\tat org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1726)\n\tat org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)\n\tat org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)\n\tat org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)\n\tat org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)\n\tat java.lang.Thread.run(Thread.java:750)\nCaused by: java.lang.reflect.InvocationTargetException\n\tat sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\n\tat sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)\n\tat sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\n\tat java.lang.reflect.Method.invoke(Method.java:498)\n\tat org.openmrs.module.webservices.rest.web.resource.impl.BaseDelegatingResource.setProperty(BaseDelegatingResource.java:722)\n\t... 95 more\nCaused by: org.openmrs.api.APIException: No rounding item specified in options. This must be set in order to use rounding for bill totals.\n\tat org.openmrs.module.billing.api.util.RoundingUtil.handleRoundingLineItem(RoundingUtil.java:100)\n\tat org.openmrs.module.billing.web.rest.resource.BillResource.setBillStatus(BillResource.java:130)\n\t... 100 more\n",
        "rawMessage": "status on class org.openmrs.module.billing.api.model.Bill",
        "translatedMessage": "status on class org.openmrs.module.billing.api.model.Bill"
    }
}

Data is same in both bills even if one was freshly evicted and fetched from the database, so this stage is too late for this kind of verification

@dkayiwa this validate function seems not a viable option from mu current understanding

Can i look at the new BillValidator class that you have written? One which follows the exact pattern of these validators: openmrs-core/api/src/main/java/org/openmrs/validator at master · openmrs/openmrs-core · GitHub

package org.openmrs.module.billing.api.validator;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;

import org.openmrs.annotation.Handler;
import org.openmrs.api.context.Context;
import org.openmrs.api.db.hibernate.DbSession;
import org.openmrs.api.db.hibernate.DbSessionFactory;
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.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;

/**
 *
 * database to perform accurate validation.
 */
@Component
@Handler(supports = { Bill.class }, order = 50)
public class BillValidator implements Validator {
	
	@Override
	public boolean supports(Class<?> clazz) {
		return Bill.class.isAssignableFrom(clazz);
	}
	
	@Override
	public void validate(Object target, Errors errors) {
		if (target == null) {
			errors.reject("billing.bill.error.null");
			return;
		}
		
		Bill bill = (Bill) target;
		
		// Only validate line item modifications for existing bills that are not pending
		if (bill.getId() != null && !bill.isPending()) {
			// Get DbSessionFactory to access Hibernate session
			DbSessionFactory sessionFactory = Context.getRegisteredComponent("dbSessionFactory", DbSessionFactory.class);
			
			if (sessionFactory != null) {
				DbSession session = sessionFactory.getCurrentSession();
				
				// Evict the instance and its line items from session cache to force fresh fetch
				// This ensures we get the original state from the database, not a cached/modified version
				if (bill.getLineItems() != null) {
					for (BillLineItem item : bill.getLineItems()) {
						session.evict(item);
					}
				}
				session.evict(bill);
				
				IBillService billService = Context.getService(IBillService.class);
				Bill originalBill = billService.getByUuid(bill.getUuid(), false);
				
				if (originalBill != null && originalBill.getLineItems() != null) {
					Set<BillLineItem> originalSet = new HashSet<>(originalBill.getLineItems());
					Set<BillLineItem> incomingSet = new HashSet<>(
					        (bill.getLineItems() != null) ? bill.getLineItems() : new ArrayList<>());
					
					if (!originalSet.equals(incomingSet)) {
						errors.rejectValue("lineItems", "billing.bill.error.lineItems.modified",
						    new Object[] { bill.getStatus() },
						    "Line items can only be modified when the bill is in PENDING state. Current status: "
						            + bill.getStatus());
					}
					
					// Clean up: evict the original bill and its line items from session
					if (originalBill.getLineItems() != null) {
						for (BillLineItem item : originalBill.getLineItems()) {
							session.evict(item);
						}
					}
					session.evict(originalBill);
				}
			}
		}
	}
}

The code fails before it reaches here as when modifying the lineItems the setBillStatus in BillResource.java , with the following error

{
    "error": {
        "message": "[status on class org.openmrs.module.billing.api.model.Bill => No rounding item specified in options. This must be set in order to use rounding for bill totals.]",
        "code": "org.openmrs.module.webservices.rest.web.resource.impl.BaseDelegatingResource:735",
        "detail": "org.openmrs.module.webservices.rest.web.response.ConversionException: status on class org.openmrs.module.billing.api.model.Bill\n\tat org.openmrs.module.webservices.rest.web.resource.impl.BaseDelegatingResource.setProperty(BaseDelegatingResource.java:735)\n\tat org.openmrs.module.webservices.rest.web.resource.impl.BaseDelegatingResource.setConvertedProperties(BaseDelegatingResource.java:606)\n\tat org.openmrs.module.webservices.rest.web.resource.impl.DelegatingCrudResource.update(DelegatingCrudResource.java:143)\n\tat org.openmrs.module.webservices.rest.web.v1_0.controller.MainResourceController.update(MainResourceController.java:134)\n\tat sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\n\tat sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)\n\tat sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\n\tat java.lang.reflect.Method.invoke(Method.java:498)\n\tat org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205)\n\tat org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:150)\n\tat org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117)\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895)\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808)\n\tat org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)\n\tat org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1072)\n\tat org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:965)\n\tat org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)\n\tat org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909)\n\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:681)\n\tat org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)\n\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:764)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.openmrs.module.web.filter.ForcePasswordChangeFilter.doFilter(ForcePasswordChangeFilter.java:61)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.openmrs.module.web.filter.ModuleFilterChain.doFilter(ModuleFilterChain.java:73)\n\tat org.openmrs.web.xss.XSSFilter.doFilter(XSSFilter.java:39)\n\tat org.openmrs.module.web.filter.ModuleFilterChain.doFilter(ModuleFilterChain.java:71)\n\tat org.openmrs.web.filter.GZIPFilter.doFilterInternal(GZIPFilter.java:66)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)\n\tat org.openmrs.module.web.filter.ModuleFilterChain.doFilter(ModuleFilterChain.java:71)\n\tat org.openmrs.module.webservices.rest.web.filter.AuthorizationFilter.doFilter(AuthorizationFilter.java:121)\n\tat org.openmrs.module.web.filter.ModuleFilterChain.doFilter(ModuleFilterChain.java:71)\n\tat org.openmrs.module.webservices.rest.web.filter.ContentTypeFilter.doFilter(ContentTypeFilter.java:64)\n\tat org.openmrs.module.web.filter.ModuleFilterChain.doFilter(ModuleFilterChain.java:71)\n\tat org.springframework.web.filter.ShallowEtagHeaderFilter.doFilterInternal(ShallowEtagHeaderFilter.java:106)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)\n\tat org.openmrs.module.web.filter.ModuleFilterChain.doFilter(ModuleFilterChain.java:71)\n\tat org.openmrs.module.authentication.web.ForcePasswordChangeFilter.doFilter(ForcePasswordChangeFilter.java:68)\n\tat org.openmrs.module.web.filter.ModuleFilterChain.doFilter(ModuleFilterChain.java:71)\n\tat org.openmrs.module.authentication.web.AuthenticationFilter.doFilter(AuthenticationFilter.java:165)\n\tat org.openmrs.module.web.filter.ModuleFilterChain.doFilter(ModuleFilterChain.java:71)\n\tat org.openmrs.module.web.filter.ModuleFilter.doFilter(ModuleFilter.java:57)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.owasp.csrfguard.CsrfGuardFilter.handleSession(CsrfGuardFilter.java:107)\n\tat org.owasp.csrfguard.CsrfGuardFilter.doFilter(CsrfGuardFilter.java:97)\n\tat org.owasp.csrfguard.CsrfGuardFilter.doFilter(CsrfGuardFilter.java:68)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.openmrs.web.filter.OpenmrsFilter.doFilterInternal(OpenmrsFilter.java:114)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.openmrs.web.filter.CookieClearingFilter.doFilterInternal(CookieClearingFilter.java:77)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.springframework.orm.hibernate5.support.OpenSessionInViewFilter.doFilterInternal(OpenSessionInViewFilter.java:156)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.springframework.web.multipart.support.MultipartFilter.doFilterInternal(MultipartFilter.java:125)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.openmrs.web.filter.StartupFilter.doFilter(StartupFilter.java:120)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.openmrs.web.filter.StartupFilter.doFilter(StartupFilter.java:120)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.openmrs.web.filter.StartupFilter.doFilter(StartupFilter.java:120)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:197)\n\tat org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)\n\tat org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542)\n\tat org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:135)\n\tat org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)\n\tat org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)\n\tat org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357)\n\tat org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:382)\n\tat org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)\n\tat org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:893)\n\tat org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1726)\n\tat org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)\n\tat org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)\n\tat org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)\n\tat org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)\n\tat java.lang.Thread.run(Thread.java:750)\nCaused by: java.lang.reflect.InvocationTargetException\n\tat sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\n\tat sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)\n\tat sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\n\tat java.lang.reflect.Method.invoke(Method.java:498)\n\tat org.openmrs.module.webservices.rest.web.resource.impl.BaseDelegatingResource.setProperty(BaseDelegatingResource.java:722)\n\t... 95 more\nCaused by: org.openmrs.api.APIException: No rounding item specified in options. This must be set in order to use rounding for bill totals.\n\tat org.openmrs.module.billing.api.util.RoundingUtil.handleRoundingLineItem(RoundingUtil.java:100)\n\tat org.openmrs.module.billing.web.rest.resource.BillResource.setBillStatus(BillResource.java:130)\n\t... 100 more\n",
        "rawMessage": "status on class org.openmrs.module.billing.api.model.Bill",
        "translatedMessage": "status on class org.openmrs.module.billing.api.model.Bill"
    }
}

It is easier to look at the class within a pull request which i can even checkout locally.

These are the changes i made, did not refine it further, as faced an error at the property setter level

Can you point me to the exact pull request url? Secondly, you do not have to manually call the validator. It is automatically called by the framework via the service layer whenever a bill gets saved.

1 Like

That’s now how the validation is implemented in this module so thats why i cannot implement the validation as expected.

This was also mentioned by Ian in the closing comments of the PR: O3-5246: Fixing the payments issue Post addition of Pending state check by jayg2002 · Pull Request #70 · openmrs/openmrs-module-billing · GitHub

Thanks @dkayiwa for giving such amazing suggestions and helping