FINERACT-2284: Enforce Charge Rounding using Money rounding rules#5940
FINERACT-2284: Enforce Charge Rounding using Money rounding rules#5940Dhanno98 wants to merge 1 commit into
Conversation
f44e89e to
38bd72e
Compare
38bd72e to
8009f8c
Compare
| minMaxCap = this.minCap; | ||
| return minMaxCap; | ||
| } | ||
| public BigDecimal minimumAndMaximumCap(final BigDecimal value) { |
There was a problem hiding this comment.
I see no operations which warrants to use Money here... this method just simply comparing values, no calculations which might led to situations where rounding is needed...
| update(loanCharge, chargeAmt, loanCharge.getDueLocalDate(), amount, loan.fetchNumberOfInstallmentsAfterExceptions(), | ||
| totalChargeAmt); | ||
|
|
||
| // Skip zero-value charges |
There was a problem hiding this comment.
Adding 0 charge is incorrect and it should be stopped at validation and or during charge amount calculation. Silent return is misleading and hiding a potential issue!
| numberOfRepayments = loanCharge.getLoan().fetchNumberOfInstallmentsAfterExceptions(); | ||
| } | ||
| loanCharge.setAmount(amount.multiply(BigDecimal.valueOf(numberOfRepayments))); | ||
| loanCharge.setAmount(loanCharge.minimumAndMaximumCap(amount.multiply(BigDecimal.valueOf(numberOfRepayments)))); |
There was a problem hiding this comment.
Unrelated change? Why loanCharge.minimumAndMaximumCap is needed here? Charge amount calculation should cover the rounding, not this...
| @Schema(example = "en") | ||
| public String locale; | ||
| @Schema(example = "dd MMMM yyyy") | ||
| public String dateFormat; | ||
| @Schema(example = "01 January 2026") | ||
| public String activatedDate; | ||
| @Schema(example = "05 May 2026") | ||
| public String requestedDate; | ||
| @Schema(example = "01 January 2026") | ||
| public String closedDate; | ||
| @Schema(description = "Can represent either number of shares or transaction IDs") | ||
| public Object requestedShares; | ||
|
|
||
| static final class PostAccountsRequestedShares { | ||
|
|
||
| private PostAccountsRequestedShares() {} | ||
|
|
||
| @Schema(example = "35") | ||
| public Long id; | ||
| } | ||
|
|
||
| public Set<PostAccountsRequestedShares> requestedShares; | ||
| } |
|
|
||
| public static ClientCharge createNew(final Client client, final Charge charge, final JsonCommand command) { | ||
| BigDecimal amount = command.bigDecimalValueOfParameterNamed(ClientApiConstants.amountParamName); | ||
| public static ClientCharge createNew(final Client client, final Charge charge, final BigDecimal amount, final JsonCommand command) { |
There was a problem hiding this comment.
If you’ve started providing the fields as incoming arguments, don’t stop halfway. Remove the command parameter and ensure that the due date is provided as a parameter.
| } | ||
|
|
||
| final ClientCharge clientCharge = ClientCharge.createNew(client, charge, command); | ||
| BigDecimal roundedAmount = calculateRoundedChargeAmount(charge, command); |
There was a problem hiding this comment.
Using Money here would be better, no?
| final ClientCharge clientCharge = ClientCharge.createNew(client, charge, command); | ||
| BigDecimal roundedAmount = calculateRoundedChargeAmount(charge, command); | ||
| if (roundedAmount.compareTo(BigDecimal.ZERO) == 0) { | ||
| return new CommandProcessingResultBuilder().withOfficeId(client.getOffice().getId()).withClientId(client.getId()).build(); |
There was a problem hiding this comment.
Why to accept 0 amount charge? 0 amount charge is incorrect outcome!
| LoanTrancheDisbursementCharge loanTrancheDisbursementCharge; | ||
| ExternalId externalId = externalIdFactory.createFromCommand(command, "externalId"); | ||
| boolean isFirst = true; | ||
| boolean atLeastOneChargePersisted = false; |
There was a problem hiding this comment.
what are these flags for?
|
|
||
| // if charge amount is rounded to zero, then continue and look for another disbursement | ||
| if (isZeroCharge(tempCharge)) { | ||
| continue; |
There was a problem hiding this comment.
0 amount charge sounds incorrect to me.
| } | ||
|
|
||
| // tempCharge was persisted so this is true | ||
| atLeastOneChargePersisted = true; |
| } | ||
| if (loanCharge == null) { | ||
| // No eligible disbursement | ||
| if (!eligibleDisbursementFound) { |
| validateAddLoanCharge(loan, chargeDefinition, loanCharge); | ||
| isAppliedOnBackDate = addCharge(loan, chargeDefinition, loanCharge); | ||
|
|
||
| // if charge amount is rounded to zero, then return early |
There was a problem hiding this comment.
Sounds incorrect result to me.
| if (!isZeroCharge(loanCharge)) { | ||
| loanCharge = this.loanChargeRepository.saveAndFlush(loanCharge); | ||
|
|
||
| // we want to apply charge transactions only for those loans charges that are applied when a loan is active |
| .notifyPostBusinessEvent(new LoanAccrualTransactionCreatedBusinessEvent(applyLoanChargeTransaction)); | ||
|
|
||
| // If charge amount is not zero | ||
| if (!isZeroCharge(loanCharge)) { |
| } | ||
| final SavingsAccountCharge savingsAccountCharge = SavingsAccountCharge.createNewFromJson(savingsAccount, chargeDefinition, command); | ||
|
|
||
| if (isFlatCharge(savingsAccountCharge) && isZeroCharge(savingsAccountCharge, savingsAccount.getCurrency())) { |
There was a problem hiding this comment.
I dont like it... 0 amount charge is incorrect result.
There was a problem hiding this comment.
I stopped review at half time.. I dont like this approach, neither i think it is correct.
The introduced 0 amount handling is incorrect. 0 amount is incorrect outcome and charge cannot be created with this value, but nevertheless, we cannot accept the charge creation action and after not create the charge. This is incorrect and unwanted behaviour:
- Add proper validation (first thing to do)
- Calculate the amount, if it is incorrect (like 0 amount) -> Raise error and explain the issue!
- Remove the unnecessary flags and checks... it makes the code hard to read and maintained...
- Remove the unrelated changes, the PR should focus on 1 thing only!
|
@adamsaghy Thanks for your suggestions! I am working on the fix. |
Description
FINERACT-2284: Enforce charge rounding using Money rounding rules
This PR fixes charge amount handling across Fineract by ensuring that charge values are rounded using the application's configured currency rules before they are persisted or processed.
Previously, several charge flows stored or processed raw
BigDecimalvalues directly. As a result, charge amounts could bypass currency rounding settings such as:digitsAfterDecimalinMultiplesOfThis led to inconsistent behavior between charge calculations and other monetary operations that already rely on
Money.This PR standardizes charge amount handling by using
Moneyfor charge rounding across all supported charge types:In addition, if a charge amount rounds to zero after applying currency rules, the charge is no longer persisted or created.
Functional Changes
Loan Charges
Savings Account Charges
Money.Share Account Charges
Client Charges
API Documentation
Updated Swagger/OpenAPI schemas where required to improve request model documentation and examples.
Why This Change
Fineract already provides currency-specific monetary rules through
Money, including support for:digitsAfterDecimal)inMultiplesOf)Charges should follow the same monetary rules as all other financial amounts.
By enforcing rounding consistently:
Test Coverage
Added integration test coverage for all supported charge types:
LoanChargeRoundingTestSavingsAccountChargeRoundingTestShareAccountChargeRoundingTestClientChargeRoundingTestThe tests verify:
inMultiplesOfanddigitsAfterDecimalrounding is enforced.Verification
Executed the complete charge rounding integration test suite successfully.
Result: 44/44 tests passing.
Checklist
Please make sure these boxes are checked before submitting your pull request - thanks!
Your assigned reviewer(s) will follow our guidelines for code reviews.