Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FINERACT-1960: Accrual Transactions For Savings batch job - Interest #3973

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from

Conversation

alberto-art3ch
Copy link
Contributor

Description

Describe the changes made and why they were made.

Financial institutions will want to recognize expenses/incomes accrued on deposit product accounts too similar to the Loan products.

In this case we are adding the Accrual Transactions For Savings batch job

FINERACT-1960

Screenshot 2024-07-15 at 5 26 05 p m

Checklist

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

  • Write the commit message as per https://github.com/apache/fineract/#pull-requests

  • 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 coding conventions at https://cwiki.apache.org/confluence/display/FINERACT/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

  • Submission is not a "code dump". (Large changes can be made "in repository" via a branch. Ask on the developer mailing list for guidance, if required.)

FYI our guidelines for code reviews are at https://cwiki.apache.org/confluence/display/FINERACT/Code+Review+Guide.

@alberto-art3ch alberto-art3ch force-pushed the enhancement/batch_job_savings_account_accruals branch 4 times, most recently from bb57643 to 0a11c8f Compare July 16, 2024 06:47
@bharathcgowda
Copy link

@adamsaghy @marta-jankovics will you be able to help us in reviewing this PR please?

]
},
{
"default": null,
Copy link
Contributor

Choose a reason for hiding this comment

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

Should it support accrued fee and penalty as well no?

@@ -57,6 +57,7 @@ public enum JobName {
SEND_ASYNCHRONOUS_EVENTS("Send Asynchronous Events"), //
PURGE_EXTERNAL_EVENTS("Purge External Events"), //
PURGE_PROCESSED_COMMANDS("Purge Processed Commands"), //
ADD_PERIODIC_ACCRUAL_ENTRIES_FOR_SAVINGS_WITH_INCOME_POSTED_AS_TRANSACTIONS("Add Accrual Transactions For Savings"); //
Copy link
Contributor

Choose a reason for hiding this comment

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

Are you sure the job name is correct? Usually there are two types of job:

  • Create periodic accruals (ADD_PERIODIC_ACCRUAL_ENTRIES_FOR_SAVINGS)
  • Create income postings (ADD_PERIODIC_ACCRUAL_ENTRIES_FOR_SAVINGS_WITH_INCOME_POSTED_AS_TRANSACTIONS vs POST_INTEREST_FOR_SAVINGS)

Which one is needed here? The job name and the description is not really matching as far as i can tell.

@@ -56,13 +56,15 @@ public class SavingsAccountSummaryData implements Serializable {
private LocalDate interestPostedTillDate;
private LocalDate prevInterestPostedTillDate;
private transient BigDecimal runningBalanceOnInterestPostingTillDate = BigDecimal.ZERO;
private LocalDate accruedTillDate;
private BigDecimal totalInterestAccrued;
Copy link
Contributor

Choose a reason for hiding this comment

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

Dont we need accrued fee and penalty?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The Accrual Fee mainly will be in other PR

Copy link
Contributor

Choose a reason for hiding this comment

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

I am wondering whether it would be better to send support for fee and penalty as well in the same PR or disable the job till it is not complete. THoughts?

final Long loanChargeId = (Long) loanChargePaid.get("savingsChargeId");
final boolean isPenalty = (Boolean) loanChargePaid.get("isPenalty");
final BigDecimal chargeAmountPaid = (BigDecimal) loanChargePaid.get("amount");
for (final Map<String, Object> savingsChargesPaid : savingsChargesPaidData) {
Copy link
Contributor

Choose a reason for hiding this comment

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

nice catch! :)

AccrualAccountsForSavings.INTEREST_ON_SAVINGS.getValue(), AccrualAccountsForSavings.INTEREST_PAYABLE.getValue(),
savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal);
if (feePayments.size() > 0 || penaltyPayments.size() > 0) {
this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode,
Copy link
Contributor

Choose a reason for hiding this comment

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

Periodic accrual for cash based accounting? @bharathcgowda

Copy link
Contributor

Choose a reason for hiding this comment

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

Would it make sense to only gather the proper credit and debit accounts into a variable and after just call one time the this.helper.createCashBasedJournalEntriesAndReversalsForSavings method?

@@ -205,35 +212,45 @@ else if (savingsTransactionDTO.getTransactionType().isFeeDeduction() && savingsT
savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, overdraftAmount, isReversal,
penaltyPayments);
if (isPositive) {
final ChargePaymentDTO chargePaymentDTO = penaltyPayments.get(0);
Copy link
Contributor

Choose a reason for hiding this comment

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

unused variable chargePaymentDTO

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done! removed

@@ -205,35 +212,45 @@ else if (savingsTransactionDTO.getTransactionType().isFeeDeduction() && savingsT
savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, overdraftAmount, isReversal,
penaltyPayments);
if (isPositive) {
final ChargePaymentDTO chargePaymentDTO = penaltyPayments.get(0);
AccrualAccountsForSavings accountTypeToBeDebited = AccrualAccountsForSavings.SAVINGS_CONTROL;
Copy link
Contributor

Choose a reason for hiding this comment

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

I am not sure why to put this into a variable. The credit pair was inline in the method... :/

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done! variable moved to be set before. This variable will be changed when we include Accrued Charges

}
} else {
this.helper.createAccrualBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode,
AccrualAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL, AccrualAccountsForSavings.INCOME_FROM_FEES,
savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, overdraftAmount, isReversal,
feePayments);
if (isPositive) {
final ChargePaymentDTO chargePaymentDTO = feePayments.get(0);
Copy link
Contributor

Choose a reason for hiding this comment

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

unused variable chargePaymentDTO

Copy link
Contributor

Choose a reason for hiding this comment

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

accountTypeToBeDebited is in variable but account to be credited does not... i dont think we need to do this at all..

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done! variable moved to be set before. This variable will be changed when we include Accrued Charges

}
}
}

else if (savingsTransactionDTO.getTransactionType().isFeeDeduction()) {
// Is the Charge a penalty?
if (penaltyPayments.size() > 0) {
final ChargePaymentDTO chargePaymentDTO = penaltyPayments.get(0);
Copy link
Contributor

Choose a reason for hiding this comment

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

unused variable chargePaymentDTO

Copy link
Contributor

Choose a reason for hiding this comment

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

accountTypeToBeDebited is in variable but account to be credited does not... i dont think we need to do this at all..

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done! variable moved to be set before. This variable will be changed when we include Accrued Charges

} else {
final ChargePaymentDTO chargePaymentDTO = feePayments.get(0);
Copy link
Contributor

Choose a reason for hiding this comment

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

unused variable chargePaymentDTO

Copy link
Contributor

Choose a reason for hiding this comment

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

accountTypeToBeDebited is in variable but account to be credited does not... i dont think we need to do this at all..

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done! variable moved to be set before. This variable will be changed when we include Accrued Charges

@@ -233,6 +235,33 @@ private PostSavingsAccountsRequest() {}
public String submittedOnDate;
@Schema(example = "123")
public String externalId;
@Schema(example = "5.0")
Copy link
Contributor

Choose a reason for hiding this comment

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

Seems unrelated change... should it be extracted into a separate PR?

Copy link
Contributor

Choose a reason for hiding this comment

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

Please dont use Double. It is either whole number and Integer / Long or BigDecimal to support fractions.

Copy link
Contributor

Choose a reason for hiding this comment

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

Please dont use Double types...

@Column(name = "accrued_till_date")
protected LocalDate accruedTillDate;

@Column(name = "total_interest_accrued_derived", scale = 6, precision = 19, nullable = true)
Copy link
Contributor

Choose a reason for hiding this comment

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

What about accrued fee and penalty?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The Accrual Fee mainly will be in other PR

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it should be handled together... but if you insist to handle differently please disable the job hence it will produce incorrect accrual transactions

xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd">
<changeSet author="fineract" id="1">
<addColumn tableName="m_savings_account">
<column defaultValueComputed="NULL" name="total_interest_accrued_derived" type="DECIMAL(19, 6)"/>
Copy link
Contributor

Choose a reason for hiding this comment

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

Should it support accrued fee or penalty?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The Accrual Fee mainly will be in other PR

import org.hamcrest.Matcher;
import org.hamcrest.Matchers;

public abstract class BaseIntegrationTest {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we need this? Most of them are duplicate from BaseLoanIntegrationTest. You might wanna have this class and change the BaseLoanIntegrationTest to extend BaseIntegrationTest and remove these common functions from BaseLoanIntegrationTest.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done! BaseIntegrationTest classes updated to use inherit

assertNotNull(transactionResponse);
assertNotNull(transactionResponse.getResourceId());

savingsAccountDetails = savingsAccountHelper.getSavingsAccount(savingsAccountId.get());
Copy link
Contributor

Choose a reason for hiding this comment

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

What does this test testing? I dont see any accrual related assertions...

// 3. Create a Savings account
// 4. Approve and Activate the Savings account
// 5. Add a Deposit
// 6.
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing steps...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done! Comments updated

    // UC1: Simple Savings account creation with Accrual Accounting enabled
    // 1. Create a client account
    // 2. Create a Savings product with Accrual accounting enabled
    // 3. Create a Savings account
    // 4. Approve and Activate the Savings account
    // 5. Add a Deposit and validate the account balance
    // ------ Using a second business date
    // 6. Add a second Deposit transaction to have other balance
    // 7. Run the new batch job to Add the Accrual transactions in the Savings account
    // 8. Get the Savings details to:
    //    a) Validate the accrued till date
    //    b) Validate the total amount of the accrual transactions generated
    //    c) Validate the Journal Entry of the first Accrual transaction generated

final GetSavingsAccountsTransaction accrualTransaction = optTransaction.get();
final List<HashMap> journalEntries = journalEntryHelper.getJournalEntriesByTransactionId("S" + accrualTransaction.getId());
assertEquals(2, journalEntries.size());
assertEquals(accrualTransaction.getAmount().floatValue(), journalEntries.get(0).get("amount"));
Copy link
Contributor

Choose a reason for hiding this comment

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

It does not check anywhere whether the proper gl account was used.

Copy link
Contributor

@adamsaghy adamsaghy left a comment

Choose a reason for hiding this comment

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

Please kindly check my review!

@adamsaghy
Copy link
Contributor

@adamsaghy @marta-jankovics will you be able to help us in reviewing this PR please?

I have done my best, but it would be nice to read a couple sentences about what this PR is intended to implement.

@alberto-art3ch alberto-art3ch force-pushed the enhancement/batch_job_savings_account_accruals branch from 0a11c8f to 4c28638 Compare July 16, 2024 17:09
@alberto-art3ch
Copy link
Contributor Author

@adamsaghy @marta-jankovics will you be able to help us in reviewing this PR please?

I have done my best, but it would be nice to read a couple sentences about what this PR is intended to implement.

Done, almost all the comments were attended or commented

@alberto-art3ch alberto-art3ch force-pushed the enhancement/batch_job_savings_account_accruals branch from 4c28638 to a99c79b Compare July 16, 2024 18:39
@@ -103,7 +103,6 @@ plugins {
id 'org.asciidoctor.jvm.pdf' version '3.3.2' apply false
id 'org.asciidoctor.jvm.epub' version '3.3.2' apply false
id 'org.asciidoctor.jvm.revealjs' version '3.3.2' apply false
id 'org.asciidoctor.jvm.gems' version '3.3.2' apply false
Copy link
Contributor

Choose a reason for hiding this comment

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

Why to remove this?

@adamsaghy adamsaghy changed the title FINERACT-1960: Accrual Transactions For Savings batch job FINERACT-1960: Accrual Transactions For Savings batch job - Interest Jul 17, 2024
Money total = Money.zero(currency);
for (final SavingsAccountTransactionData transaction : transactions) {
if (transaction.isAccrual() && !transaction.isReversalTransaction()) {
total = total.plus(transaction.getAmount());
Copy link
Contributor

Choose a reason for hiding this comment

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

THis will be incorrect the moment accrual will be done for fee and penalties as well. I think we should immediately use the interest portion here, instead of the transaction amount.

@@ -59,23 +59,23 @@ public void createJournalEntriesForSavings(final SavingsDTO savingsDTO) {
if (savingsTransactionDTO.getTransactionType().isWithdrawal() && savingsTransactionDTO.isOverdraftTransaction()) {
boolean isPositive = amount.subtract(overdraftAmount).compareTo(BigDecimal.ZERO) > 0;
if (savingsTransactionDTO.isAccountTransfer()) {
this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode,
Copy link
Contributor

Choose a reason for hiding this comment

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

I am not sure this is correct. You just changed from cash based journal entries to accrual based one without any condition. I am assuming accounting wise after this PR cash based and accrual based can be selected and both should work. But here you are overriding the behaviour. Am i missing something?

this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode,
AccrualAccountsForSavings.INTEREST_ON_SAVINGS.getValue(), AccrualAccountsForSavings.INTEREST_PAYABLE.getValue(),
savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal);
if (feePayments.size() > 0 || penaltyPayments.size() > 0) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Seems you are trying to support fees and penalties, so i reckon the PR should involve accrual handling for them as well.

@@ -198,6 +205,7 @@ else if (savingsTransactionDTO.getTransactionType().isWithholdTax()) {
/** Handle Fees Deductions and reversals of Fees Deductions **/
else if (savingsTransactionDTO.getTransactionType().isFeeDeduction() && savingsTransactionDTO.isOverdraftTransaction()) {
boolean isPositive = amount.subtract(overdraftAmount).compareTo(BigDecimal.ZERO) > 0;
AccrualAccountsForSavings accountTypeToBeDebited = AccrualAccountsForSavings.SAVINGS_CONTROL;
Copy link
Contributor

Choose a reason for hiding this comment

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

Why to extract this into a field? the accountTypeToBeCredited still inline...

}
}
}

else if (savingsTransactionDTO.getTransactionType().isFeeDeduction()) {
AccrualAccountsForSavings accountTypeToBeCredited = AccrualAccountsForSavings.INCOME_FROM_PENALTIES;
Copy link
Contributor

Choose a reason for hiding this comment

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

Why to extract this into a field? the accountTypeToBeCredited still inline...

}
}

/** Handle Transfers proposal **/
else if (savingsTransactionDTO.getTransactionType().isInitiateTransfer()) {
this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode,
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do you override the cash based journal entry handling?

AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), AccrualAccountsForSavings.TRANSFERS_SUSPENSE.getValue(),
savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal);
}

/** Handle Transfer Withdrawal or Acceptance **/
else if (savingsTransactionDTO.getTransactionType().isWithdrawTransfer()
|| savingsTransactionDTO.getTransactionType().isApproveTransfer()) {
this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode,
this.helper.createAccrualBasedJournalEntriesAndReversalsForSavings(office, currencyCode,
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do you override the cash based journal entry handling?

AccrualAccountsForSavings.TRANSFERS_SUSPENSE.getValue(), AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(),
savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal);
}

/** overdraft **/
else if (savingsTransactionDTO.getTransactionType().isOverdraftInterest()) {
this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode,
this.helper.createAccrualBasedJournalEntriesAndReversalsForSavings(office, currencyCode,
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do you override the cash based journal entry handling?

AccrualAccountsForSavings.SAVINGS_REFERENCE.getValue(), AccrualAccountsForSavings.INCOME_FROM_INTEREST.getValue(),
savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal);
} else if (savingsTransactionDTO.getTransactionType().isWrittenoff()) {
this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode,
this.helper.createAccrualBasedJournalEntriesAndReversalsForSavings(office, currencyCode,
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do you override the cash based journal entry handling?

final boolean isReversed = false;
final Boolean lienTransaction = false;
final String refNo = ExternalId.generate().getValue();
return new SavingsAccountTransaction(savingsAccount, office, SavingsAccountTransactionType.ACCRUAL.getValue(), date, amount,
Copy link
Contributor

Choose a reason for hiding this comment

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

I think balance portions are important: fee, penalty and interest portion of the transaction.

}
}
if (!errors.isEmpty()) {
throw new JobExecutionException(errors);
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this error logic should be moved into the job... JobExecutionException seems pretty job specific error.

}
}

public boolean isChargeToBeRecognizedAsAccrual(final Collection<Long> chargeIds, final SavingsAccountCharge savingsAccountCharge) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This seems to me a logic that does not really required to be a method... you are checking whether the 1st parameter collection contains the charge id of the 2nd parameter...

SavingsAccountCharge savingsAccountCharge, LocalDate transactionDate) {
final MonetaryCurrency currency = savingsAccount.getCurrency();
final Money chargeAmount = savingsAccountCharge.getAmount(currency);
SavingsAccountTransaction savingsAccountTransaction = SavingsAccountTransaction.accrual(savingsAccount, savingsAccount.office(),
Copy link
Contributor

Choose a reason for hiding this comment

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

You are creating accrual for a charge... so charges should be supported along with interest, no?

@adamsaghy
Copy link
Contributor

@alberto-art3ch Kindly see my review!

Copy link

This pull request seems to be stale. Are you still planning to work on it? We will automatically close it in 30 days.

@github-actions github-actions bot added the stale label Aug 17, 2024
@galovics
Copy link
Contributor

@alberto-art3ch is this being worked on?

@github-actions github-actions bot removed the stale label Sep 12, 2024
@galovics
Copy link
Contributor

galovics commented Oct 4, 2024

@alberto-art3ch can you provide an update on this please?

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.

4 participants