Skip to content

FINERACT-2284: Enforce Charge Rounding using Money rounding rules#5940

Open
Dhanno98 wants to merge 1 commit into
apache:developfrom
Dhanno98:FINERACT-2284/enforce-charge-rounding
Open

FINERACT-2284: Enforce Charge Rounding using Money rounding rules#5940
Dhanno98 wants to merge 1 commit into
apache:developfrom
Dhanno98:FINERACT-2284/enforce-charge-rounding

Conversation

@Dhanno98

@Dhanno98 Dhanno98 commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

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 BigDecimal values directly. As a result, charge amounts could bypass currency rounding settings such as:

  • digitsAfterDecimal
  • inMultiplesOf

This led to inconsistent behavior between charge calculations and other monetary operations that already rely on Money.

This PR standardizes charge amount handling by using Money for charge rounding across all supported charge types:

  • Loan Charges
  • Savings Account Charges
  • Share Account Charges
  • Client Charges

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

  • Applied currency rounding when enforcing minimum and maximum charge caps.
  • Ensured installment fee calculations and charge updates persist rounded values.
  • Prevented zero-value loan charges from being persisted.

Savings Account Charges

  • Applied currency rounding through Money.
  • Prevented persistence of flat charges whose final rounded value is zero.

Share Account Charges

  • Applied currency rounding when deriving charge amounts.
  • Applied currency rounding when updating charge amounts for additional share transactions.
  • Prevented creation of charge transactions for charges whose rounded value is zero.
  • Prevented zero-value activation, purchase, and redemption charge transactions from being generated.

Client Charges

  • Applied currency rounding before charge creation.
  • Prevented persistence of client charges whose rounded value becomes zero after rounding.

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:

  • Currency precision (digitsAfterDecimal)
  • Rounding increments (inMultiplesOf)

Charges should follow the same monetary rules as all other financial amounts.

By enforcing rounding consistently:

  • Charge calculations become predictable.
  • Persisted charge values match configured currency behavior.
  • Zero-value charges generated by rounding are avoided.
  • Charge processing remains consistent across all portfolio modules.

Test Coverage

Added integration test coverage for all supported charge types:

  • LoanChargeRoundingTest
  • SavingsAccountChargeRoundingTest
  • ShareAccountChargeRoundingTest
  • ClientChargeRoundingTest

The tests verify:

  • Charge amounts respect configured currency rounding rules.
  • inMultiplesOf and digitsAfterDecimal rounding is enforced.
  • Rounded values are persisted correctly.
  • Charges that round to zero are not created or persisted.

Verification

Executed the complete charge rounding integration test suite successfully.

Checklist

Please make sure these boxes are checked before submitting your pull request - thanks!

  • Write the commit message as per our guidelines
  • Acknowledge that we will not review PRs that are not passing the build ("green") - it is your responsibility to get a proposed PR to pass the build, not primarily the project's maintainers.
  • Create/update unit or integration tests for verifying the changes made.
  • Follow our coding conventions.
  • Add required Swagger annotation and update API documentation at fineract-provider/src/main/resources/static/legacy-docs/apiLive.htm with details of any API changes
  • This PR must not be a "code dump". Large changes can be made in a branch, with assistance. Ask for help on the developer mailing list.

Your assigned reviewer(s) will follow our guidelines for code reviews.

@Dhanno98 Dhanno98 force-pushed the FINERACT-2284/enforce-charge-rounding branch 2 times, most recently from 38bd72e to 8009f8c Compare June 6, 2026 18:06
update(loanCharge, chargeAmt, loanCharge.getDueLocalDate(), amount, loan.fetchNumberOfInstallmentsAfterExceptions(),
totalChargeAmt);

// Skip zero-value charges

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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!

@adamsaghy adamsaghy Jun 18, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

See no changes...no reply...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. The silent return for zero-amount charges has been removed. A validation check is added in LoanChargeService.addLoanCharge(). If the calculated charge amount becomes zero after applying rounding rules, a LoanChargeCannotBeAddedException is now raised with the message error.msg.loanCharge.cannot.be.added.as.amount.rounded.to.zero. This prevents the charge from being created and returns an explicit validation error to the client instead of silently ignoring the request.

numberOfRepayments = loanCharge.getLoan().fetchNumberOfInstallmentsAfterExceptions();
}
loanCharge.setAmount(amount.multiply(BigDecimal.valueOf(numberOfRepayments)));
loanCharge.setAmount(loanCharge.minimumAndMaximumCap(amount.multiply(BigDecimal.valueOf(numberOfRepayments))));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Unrelated change? Why loanCharge.minimumAndMaximumCap is needed here? Charge amount calculation should cover the rounding, not this...

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

See no changes...no reply...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You're right. I originally reused minimumAndMaximumCap() because percentage-based charges already passed through that path and received currency-based rounding there. However, that couples rounding with cap enforcement. I've removed the call from the flat charge flow and now perform the rounding directly during flat charge amount calculation.

Comment on lines 468 to 488
@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;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Unrelated changes?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

See no changes...no reply...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The reason for this change is that I needed to add integration test coverage for the Share Account charge rounding flow introduced in this PR. I intentionally used the generated Fineract client APIs instead of the deprecated RestAssured helpers, since the project is gradually moving toward client-based integration tests.

While implementing the Share Account tests, I found that PostAccountsTypeAccountIdRequest was incomplete in the generated client because the OpenAPI schema only exposed requestedShares. However, share account commands used by the tests (activate, redeemshares, etc.) require fields such as locale, dateFormat, activatedDate, and requestedDate. Without exposing those fields in the OpenAPI definition, the generated client model could not be used to implement these test flows.

So the schema update was made to enable the ShareAccountChargeRoundingTest that covers the rounding changes, rather than as an independent Swagger cleanup.
That said, if you'd prefer this to be split into a separate PR, I'm happy to do that.


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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

See no changes...no reply...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done! I removed the command parameter, and now the due date is provided as a parameter.

}

final ClientCharge clientCharge = ClientCharge.createNew(client, charge, command);
BigDecimal roundedAmount = calculateRoundedChargeAmount(charge, command);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Using Money here would be better, no?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

See no changes...no reply...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done! I have used of Money instead of BigDecimal here in the latest changes.

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();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why to accept 0 amount charge? 0 amount charge is incorrect outcome!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

See no changes...no reply...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, it is incorrect, and I have made the change to throw now a PlatformApiDataValidationException when the final charge amount rounds to 0.

LoanTrancheDisbursementCharge loanTrancheDisbursementCharge;
ExternalId externalId = externalIdFactory.createFromCommand(command, "externalId");
boolean isFirst = true;
boolean atLeastOneChargePersisted = false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

what are these flags for?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

See no changes...no reply...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Reverted. These changes were not required for the final solution and have been removed.


// if charge amount is rounded to zero, then continue and look for another disbursement
if (isZeroCharge(tempCharge)) {
continue;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

0 amount charge sounds incorrect to me.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Reverted.

}

// tempCharge was persisted so this is true
atLeastOneChargePersisted = true;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

what does it matter?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Reverted. I was understanding things incorrectly here. So not needed.

}
if (loanCharge == null) {
// No eligible disbursement
if (!eligibleDisbursementFound) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I dont like this...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Reverted.

validateAddLoanCharge(loan, chargeDefinition, loanCharge);
isAppliedOnBackDate = addCharge(loan, chargeDefinition, loanCharge);

// if charge amount is rounded to zero, then return early

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sounds incorrect result to me.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Reverted.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why to change this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Reverted. This was not needed.

.notifyPostBusinessEvent(new LoanAccrualTransactionCreatedBusinessEvent(applyLoanChargeTransaction));

// If charge amount is not zero
if (!isZeroCharge(loanCharge)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I dont like this..

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Reverted.

}
final SavingsAccountCharge savingsAccountCharge = SavingsAccountCharge.createNewFromJson(savingsAccount, chargeDefinition, command);

if (isFlatCharge(savingsAccountCharge) && isZeroCharge(savingsAccountCharge, savingsAccount.getCurrency())) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I dont like it... 0 amount charge is incorrect result.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good point. I updated the implementation to prevent zero-amount savings charges from being persisted when the final rounded amount is known at charge attachment time.

After the charge amount is rounded according to the savings product currency configuration (digitsAfterDecimal/inMultiplesOf), I validate the resulting amount before persistence. If the rounded amount becomes zero, a validation error is returned, and the charge is not added.

This validation is currently applied only to flat savings charges because percentage-based charges (for eg withdrawal fees) are calculated later during transaction processing, so the final charge amount is not available when the charge is attached.

@adamsaghy adamsaghy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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!

@Dhanno98

Copy link
Copy Markdown
Contributor Author

@adamsaghy Thanks for your suggestions! I am working on the fix.

@Dhanno98

Copy link
Copy Markdown
Contributor Author

Hi @adamsaghy,
I have updated the charge attachment flows locally to reject charges whose calculated amount becomes 0 after applying account currency rounding rules, instead of returning silently.

I have a question regarding future charges such as Savings Account Percentage of Withdrawal Charges and Share Account Redeem Charges.

In these cases, the charge is attached successfully first and the amount is calculated only when the business transaction occurs. If the calculated charge amount becomes 0 after rounding, what is the expected behavior:

  1. Reject the business transaction (withdrawal/redeem) with a validation error.
  2. Allow the transaction but skip creating/applying the charge similar to silent return.
  3. Keep the current behavior and allow a 0 value charge result.

Could you please clarify the expected behavior for these future-charge scenarios so I can align the implementation consistently across Savings and Shares?

Thank you!

@adamsaghy

Copy link
Copy Markdown
Contributor

Hi @adamsaghy, I have updated the charge attachment flows locally to reject charges whose calculated amount becomes 0 after applying account currency rounding rules, instead of returning silently.

I have a question regarding future charges such as Savings Account Percentage of Withdrawal Charges and Share Account Redeem Charges.

In these cases, the charge is attached successfully first and the amount is calculated only when the business transaction occurs. If the calculated charge amount becomes 0 after rounding, what is the expected behavior:

  1. Reject the business transaction (withdrawal/redeem) with a validation error.
  2. Allow the transaction but skip creating/applying the charge similar to silent return.
  3. Keep the current behavior and allow a 0 value charge result.

Could you please clarify the expected behavior for these future-charge scenarios so I can align the implementation consistently across Savings and Shares?

Thank you!

@bharathc27 fyi

@Dhanno98 let me review

@adamsaghy

Copy link
Copy Markdown
Contributor

Hi @adamsaghy, I have updated the charge attachment flows locally to reject charges whose calculated amount becomes 0 after applying account currency rounding rules, instead of returning silently.

I have a question regarding future charges such as Savings Account Percentage of Withdrawal Charges and Share Account Redeem Charges.

In these cases, the charge is attached successfully first and the amount is calculated only when the business transaction occurs. If the calculated charge amount becomes 0 after rounding, what is the expected behavior:

  1. Reject the business transaction (withdrawal/redeem) with a validation error.
  2. Allow the transaction but skip creating/applying the charge similar to silent return.
  3. Keep the current behavior and allow a 0 value charge result.

Could you please clarify the expected behavior for these future-charge scenarios so I can align the implementation consistently across Savings and Shares?

Thank you!

I see no changes, no reply on my questions and concerns.. i have stopped reviewing the PR at half time...

@Dhanno98

Dhanno98 commented Jun 18, 2026

Copy link
Copy Markdown
Contributor Author

Hi @adamsaghy, I have updated the charge attachment flows locally to reject charges whose calculated amount becomes 0 after applying account currency rounding rules, instead of returning silently.
I have a question regarding future charges such as Savings Account Percentage of Withdrawal Charges and Share Account Redeem Charges.
In these cases, the charge is attached successfully first and the amount is calculated only when the business transaction occurs. If the calculated charge amount becomes 0 after rounding, what is the expected behavior:

  1. Reject the business transaction (withdrawal/redeem) with a validation error.
  2. Allow the transaction but skip creating/applying the charge similar to silent return.
  3. Keep the current behavior and allow a 0 value charge result.

Could you please clarify the expected behavior for these future-charge scenarios so I can align the implementation consistently across Savings and Shares?
Thank you!

I see no changes, no reply on my questions and concerns.. i have stopped reviewing the PR at half time...

Hi @adamsaghy,

Sorry for the confusion. When I wrote that I had updated the charge attachment flows locally, I meant that the changes were only completed in my local branch and had not yet been pushed or amended into the PR.

My intention was to first finish addressing all of your review comments, validate everything thoroughly, and then push a single updated commit together with replies to each review comment. Because of that, I had not yet responded individually to the review feedback.

I realize now that my comment made it sound like the PR had already been updated, which was not the case. Sorry for causing you to spend time reopening the review.

I am currently working through all of the review comments and will push the updated changes together with responses shortly.

Regarding the future-charge scenarios, that question was meant as a design clarification while implementing the remaining changes so I can keep the behavior consistent across Savings and Shares. This is the only thing that I need to understand and resolve and then I will ammend the commit.

Thank you for your patience and for the detailed review feedback.

@adamsaghy

Copy link
Copy Markdown
Contributor

Hi @adamsaghy, I have updated the charge attachment flows locally to reject charges whose calculated amount becomes 0 after applying account currency rounding rules, instead of returning silently.
I have a question regarding future charges such as Savings Account Percentage of Withdrawal Charges and Share Account Redeem Charges.
In these cases, the charge is attached successfully first and the amount is calculated only when the business transaction occurs. If the calculated charge amount becomes 0 after rounding, what is the expected behavior:

  1. Reject the business transaction (withdrawal/redeem) with a validation error.
  2. Allow the transaction but skip creating/applying the charge similar to silent return.
  3. Keep the current behavior and allow a 0 value charge result.

Could you please clarify the expected behavior for these future-charge scenarios so I can align the implementation consistently across Savings and Shares?
Thank you!

I see no changes, no reply on my questions and concerns.. i have stopped reviewing the PR at half time...

Hi @adamsaghy,

Sorry for the confusion. When I wrote that I had updated the charge attachment flows locally, I meant that the changes were only completed in my local branch and had not yet been pushed or amended into the PR.

My intention was to first finish addressing all of your review comments, validate everything thoroughly, and then push a single updated commit together with replies to each review comment. Because of that, I had not yet responded individually to the review feedback.

I realize now that my comment made it sound like the PR had already been updated, which was not the case. Sorry for causing you to spend time reopening the review.

I am currently working through all of the review comments and will push the updated changes together with responses shortly.

Regarding the future-charge scenarios, that question was meant as a design clarification while implementing the remaining changes so I can keep the behavior consistent across Savings and Shares. This is the only thing that I need to understand and resolve and then I will ammend the commit.

Thank you for your patience and for the detailed review feedback.

Thank you for the clarification ;)

Regarding zero amount: Lets use option 1 Reject the business transaction (withdrawal/redeem) with a validation error.

@Dhanno98

Copy link
Copy Markdown
Contributor Author

Hi @adamsaghy, I have updated the charge attachment flows locally to reject charges whose calculated amount becomes 0 after applying account currency rounding rules, instead of returning silently.
I have a question regarding future charges such as Savings Account Percentage of Withdrawal Charges and Share Account Redeem Charges.
In these cases, the charge is attached successfully first and the amount is calculated only when the business transaction occurs. If the calculated charge amount becomes 0 after rounding, what is the expected behavior:

  1. Reject the business transaction (withdrawal/redeem) with a validation error.
  2. Allow the transaction but skip creating/applying the charge similar to silent return.
  3. Keep the current behavior and allow a 0 value charge result.

Could you please clarify the expected behavior for these future-charge scenarios so I can align the implementation consistently across Savings and Shares?
Thank you!

I see no changes, no reply on my questions and concerns.. i have stopped reviewing the PR at half time...

Hi @adamsaghy,
Sorry for the confusion. When I wrote that I had updated the charge attachment flows locally, I meant that the changes were only completed in my local branch and had not yet been pushed or amended into the PR.
My intention was to first finish addressing all of your review comments, validate everything thoroughly, and then push a single updated commit together with replies to each review comment. Because of that, I had not yet responded individually to the review feedback.
I realize now that my comment made it sound like the PR had already been updated, which was not the case. Sorry for causing you to spend time reopening the review.
I am currently working through all of the review comments and will push the updated changes together with responses shortly.
Regarding the future-charge scenarios, that question was meant as a design clarification while implementing the remaining changes so I can keep the behavior consistent across Savings and Shares. This is the only thing that I need to understand and resolve and then I will ammend the commit.
Thank you for your patience and for the detailed review feedback.

Thank you for the clarification ;)

Regarding zero amount: Lets use option 1 Reject the business transaction (withdrawal/redeem) with a validation error.

Thank you for the clarification.

Just to confirm the expected behavior for percentage-based future charges:

Suppose a Savings Account has a "Percentage of Withdrawal" charge configured as 0.5%.

When the charge is attached to the account, the percentage value (0.5%) is stored successfully because the actual charge amount is not yet known.

Later, if a withdrawal of 100 is performed and the account currency uses 0 decimal places (for example JPY), the calculated charge amount would be:

0.5% × 100 = 0.5

which would then round to 0 according to the currency rounding rules.

With option 1, my understanding is that the withdrawal transaction itself should be rejected with a validation error because the calculated charge amount becomes zero after rounding.

Is that the intended behavior, even though the withdrawal amount itself is valid and only the associated charge rounds to zero?

I just want to confirm this edge case before implementing the validation in the future-charge flows.

@adamsaghy

Copy link
Copy Markdown
Contributor

Hi @adamsaghy, I have updated the charge attachment flows locally to reject charges whose calculated amount becomes 0 after applying account currency rounding rules, instead of returning silently.
I have a question regarding future charges such as Savings Account Percentage of Withdrawal Charges and Share Account Redeem Charges.
In these cases, the charge is attached successfully first and the amount is calculated only when the business transaction occurs. If the calculated charge amount becomes 0 after rounding, what is the expected behavior:

  1. Reject the business transaction (withdrawal/redeem) with a validation error.
  2. Allow the transaction but skip creating/applying the charge similar to silent return.
  3. Keep the current behavior and allow a 0 value charge result.

Could you please clarify the expected behavior for these future-charge scenarios so I can align the implementation consistently across Savings and Shares?
Thank you!

I see no changes, no reply on my questions and concerns.. i have stopped reviewing the PR at half time...

Hi @adamsaghy,
Sorry for the confusion. When I wrote that I had updated the charge attachment flows locally, I meant that the changes were only completed in my local branch and had not yet been pushed or amended into the PR.
My intention was to first finish addressing all of your review comments, validate everything thoroughly, and then push a single updated commit together with replies to each review comment. Because of that, I had not yet responded individually to the review feedback.
I realize now that my comment made it sound like the PR had already been updated, which was not the case. Sorry for causing you to spend time reopening the review.
I am currently working through all of the review comments and will push the updated changes together with responses shortly.
Regarding the future-charge scenarios, that question was meant as a design clarification while implementing the remaining changes so I can keep the behavior consistent across Savings and Shares. This is the only thing that I need to understand and resolve and then I will ammend the commit.
Thank you for your patience and for the detailed review feedback.

Thank you for the clarification ;)
Regarding zero amount: Lets use option 1 Reject the business transaction (withdrawal/redeem) with a validation error.

Thank you for the clarification.

Just to confirm the expected behavior for percentage-based future charges:

Suppose a Savings Account has a "Percentage of Withdrawal" charge configured as 0.5%.

When the charge is attached to the account, the percentage value (0.5%) is stored successfully because the actual charge amount is not yet known.

Later, if a withdrawal of 100 is performed and the account currency uses 0 decimal places (for example JPY), the calculated charge amount would be:

0.5% × 100 = 0.5

which would then round to 0 according to the currency rounding rules.

With option 1, my understanding is that the withdrawal transaction itself should be rejected with a validation error because the calculated charge amount becomes zero after rounding.

Is that the intended behavior, even though the withdrawal amount itself is valid and only the associated charge rounds to zero?

I just want to confirm this edge case before implementing the validation in the future-charge flows.

With this use case, it’s not as straightforward as it seems. I might have been overly strict here. Just because the charge is too small, we still want to allow the withdrawal. So, if the calculated charge is zero, we simply ignore it and don’t create it.

@bharathc27, what are your thoughts on this?

@bharathcgowda

bharathcgowda commented Jun 18, 2026

Copy link
Copy Markdown

@adamsaghy @Dhanno98 In any entity and scenario if the calculated charge amount is zero (after rounding) that charge should not be created.
In your latest example, "Allow the transaction but skip creating/applying the charge, similar to silent return," sounds right to me.
Any transactions associated with charges should not be affected. Hence withdraw transaction happens with no charge posting in the above example

@Dhanno98

Copy link
Copy Markdown
Contributor Author

@adamsaghy @Dhanno98 In any entity and scenario if the calculated charge amount is zero (after rounding) that charge should not be created. In your latest example, "Allow the transaction but skip creating/applying the charge, similar to silent return," sounds right to me. Any transactions associated with charges should not be affected. Hence withdraw transaction happens with no charge posting in the above example

Hi @bharathcgowda @adamsaghy
One follow-up question regarding future charges.

For percentage-based future charges (withdrawal/redeem), I understand that the charge definition should be stored and, when the calculated amount rounds to zero, the transaction should continue and the charge should simply not be applied.

For flat future charges (for example a Shares Flat Redeem Charge of 0.5 in a currency with 0 decimal places), the rounded charge amount is known to be zero already at attachment time and can never become non-zero later.

Currently, the existing behavior appears to allow such a Flat Redeem Charge to be attached to the share account without any validation. During the attachment flow, the configured charge amount (0.5 in this example) is stored as-is and no currency rounding is performed. The amount is only passed through Money.of() later during the redeem-shares flow, where the calculated charge amount becomes 0 and is effectively not applied.

For this scenario, what should be the expected behavior:

  1. Keep the current behavior: allow the charge to be attached as-is and, during redemption, simply ignore it if the rounded amount becomes 0, similar to Percentage of Redeem Charges.

  2. Round the flat redeem charge during attachment and reject it if the amount rounds to 0 by throwing a validation exception in the charge attachment flow itself.

@adamsaghy

Copy link
Copy Markdown
Contributor

@adamsaghy @Dhanno98 In any entity and scenario if the calculated charge amount is zero (after rounding) that charge should not be created. In your latest example, "Allow the transaction but skip creating/applying the charge, similar to silent return," sounds right to me. Any transactions associated with charges should not be affected. Hence withdraw transaction happens with no charge posting in the above example

Hi @bharathcgowda @adamsaghy One follow-up question regarding future charges.

For percentage-based future charges (withdrawal/redeem), I understand that the charge definition should be stored and, when the calculated amount rounds to zero, the transaction should continue and the charge should simply not be applied.

For flat future charges (for example a Shares Flat Redeem Charge of 0.5 in a currency with 0 decimal places), the rounded charge amount is known to be zero already at attachment time and can never become non-zero later.

Currently, the existing behavior appears to allow such a Flat Redeem Charge to be attached to the share account without any validation. During the attachment flow, the configured charge amount (0.5 in this example) is stored as-is and no currency rounding is performed. The amount is only passed through Money.of() later during the redeem-shares flow, where the calculated charge amount becomes 0 and is effectively not applied.

For this scenario, what should be the expected behavior:

  1. Keep the current behavior: allow the charge to be attached as-is and, during redemption, simply ignore it if the rounded amount becomes 0, similar to Percentage of Redeem Charges.
  2. Round the flat redeem charge during attachment and reject it if the amount rounds to 0 by throwing a validation exception in the charge attachment flow itself.

I dont think we should reject and throw error if charge became 0... @bharathcgowda thoughts?

@Dhanno98 Dhanno98 force-pushed the FINERACT-2284/enforce-charge-rounding branch from 8009f8c to 84f7719 Compare June 22, 2026 17:21
@Dhanno98 Dhanno98 force-pushed the FINERACT-2284/enforce-charge-rounding branch from 84f7719 to 8a82b79 Compare June 23, 2026 06:28
@Dhanno98

Copy link
Copy Markdown
Contributor Author

@adamsaghy @Dhanno98 In any entity and scenario if the calculated charge amount is zero (after rounding) that charge should not be created. In your latest example, "Allow the transaction but skip creating/applying the charge, similar to silent return," sounds right to me. Any transactions associated with charges should not be affected. Hence withdraw transaction happens with no charge posting in the above example

Hi @bharathcgowda @adamsaghy One follow-up question regarding future charges.
For percentage-based future charges (withdrawal/redeem), I understand that the charge definition should be stored and, when the calculated amount rounds to zero, the transaction should continue and the charge should simply not be applied.
For flat future charges (for example a Shares Flat Redeem Charge of 0.5 in a currency with 0 decimal places), the rounded charge amount is known to be zero already at attachment time and can never become non-zero later.
Currently, the existing behavior appears to allow such a Flat Redeem Charge to be attached to the share account without any validation. During the attachment flow, the configured charge amount (0.5 in this example) is stored as-is and no currency rounding is performed. The amount is only passed through Money.of() later during the redeem-shares flow, where the calculated charge amount becomes 0 and is effectively not applied.
For this scenario, what should be the expected behavior:

  1. Keep the current behavior: allow the charge to be attached as-is and, during redemption, simply ignore it if the rounded amount becomes 0, similar to Percentage of Redeem Charges.
  2. Round the flat redeem charge during attachment and reject it if the amount rounds to 0 by throwing a validation exception in the charge attachment flow itself.

I dont think we should reject and throw error if charge became 0... @bharathcgowda thoughts?

Thanks @adamsaghy.

The current implementation in this PR already follows that behavior for Share Redeem Charges and Savings Percentage Withdrawal Charges.

These charges continue to be attached and stored as configured. When they are later applied, the calculated charge amount goes through the existing Money rounding rules and, if the rounded amount becomes 0, the transaction proceeds normally and the charge is simply not applied.

So Share Flat Redeem Charges that eventually round to 0 during the Redeem Shares flow are currently not rejected during attachment.

I've also updated the PR with the latest changes and fixes.

@Dhanno98 Dhanno98 force-pushed the FINERACT-2284/enforce-charge-rounding branch from 8a82b79 to 1bcae1a Compare June 24, 2026 07:00
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