From a99c79bc5a641015fbd2bc9d69aa119579a3ae99 Mon Sep 17 00:00:00 2001 From: Jose Alberto Hernandez Date: Mon, 15 Jul 2024 17:30:31 -0600 Subject: [PATCH] FINERACT-1960: Accrual Transactions For Savings batch job --- build.gradle | 1 - .../v1/SavingsAccountSummaryDataV1.avsc | 16 ++ .../infrastructure/jobs/service/JobName.java | 1 + .../staff/domain/StaffRepository.java | 0 .../staff/domain/StaffRepositoryWrapper.java | 0 .../domain/ClientRepositoryWrapper.java | 0 .../group/domain/GroupRepository.java | 0 .../group/domain/GroupRepositoryWrapper.java | 0 .../exception/CenterNotActiveException.java | 0 .../exception/GroupNotFoundException.java | 0 .../data/SavingsAccountSummaryData.java | 6 +- .../data/SavingsAccountTransactionData.java | 4 + ...sAccountTransactionDataSummaryWrapper.java | 11 + .../service/AccountingProcessorHelper.java | 26 +- ...ualBasedAccountingProcessorForSavings.java | 75 +++--- .../SavingsAccountsApiResourceSwagger.java | 247 ++++++++++++++++++ .../SavingsProductsApiResourceSwagger.java | 39 ++- .../savings/domain/FixedDepositAccount.java | 2 +- .../domain/RecurringDepositAccount.java | 2 +- ...DepositAccountReadPlatformServiceImpl.java | 6 +- ...SavingsAccountReadPlatformServiceImpl.java | 111 +++++++- fineract-savings/dependencies.gradle | 2 + .../savings/data/SavingsAccrualData.java | 52 ++++ .../savings/domain/SavingsAccount.java | 24 +- .../domain/SavingsAccountAssembler.java | 12 +- .../savings/domain/SavingsAccountSummary.java | 73 +----- .../domain/SavingsAccountTransaction.java | 14 + ...AddAccrualTransactionForSavingsConfig.java | 60 +++++ ...ddAccrualTransactionForSavingsTasklet.java | 50 ++++ .../SavingsAccountReadPlatformService.java | 5 + .../SavingsAccrualDomainServiceImpl.java | 196 ++++++++++++++ .../{parts => }/module-changelog-master.xml | 1 + ...01_add_accrued_data_to_savings_account.xml | 73 ++++++ .../integrationtests/BaseIntegrationTest.java | 101 +++++++ .../BaseLoanIntegrationTest.java | 54 +--- .../BaseSavingsIntegrationTest.java | 114 ++++++++ .../client/IntegrationTest.java | 3 + .../common/savings/SavingsAccountHelper.java | 40 +++ .../common/savings/SavingsProductHelper.java | 13 +- .../accrual/SavingsAccrualAccountingTest.java | 190 ++++++++++++++ 40 files changed, 1447 insertions(+), 177 deletions(-) rename {fineract-provider => fineract-core}/src/main/java/org/apache/fineract/organisation/staff/domain/StaffRepository.java (100%) rename {fineract-provider => fineract-core}/src/main/java/org/apache/fineract/organisation/staff/domain/StaffRepositoryWrapper.java (100%) rename {fineract-provider => fineract-core}/src/main/java/org/apache/fineract/portfolio/client/domain/ClientRepositoryWrapper.java (100%) rename {fineract-provider => fineract-core}/src/main/java/org/apache/fineract/portfolio/group/domain/GroupRepository.java (100%) rename {fineract-provider => fineract-core}/src/main/java/org/apache/fineract/portfolio/group/domain/GroupRepositoryWrapper.java (100%) rename {fineract-provider => fineract-core}/src/main/java/org/apache/fineract/portfolio/group/exception/CenterNotActiveException.java (100%) rename {fineract-provider => fineract-core}/src/main/java/org/apache/fineract/portfolio/group/exception/GroupNotFoundException.java (100%) create mode 100644 fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccrualData.java rename {fineract-provider => fineract-savings}/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountAssembler.java (98%) create mode 100644 fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/jobs/addaccrualtransactionforsavings/AddAccrualTransactionForSavingsConfig.java create mode 100644 fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/jobs/addaccrualtransactionforsavings/AddAccrualTransactionForSavingsTasklet.java create mode 100644 fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/accrual/SavingsAccrualDomainServiceImpl.java rename fineract-savings/src/main/resources/db/changelog/tenant/module/savings/{parts => }/module-changelog-master.xml (92%) create mode 100644 fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/0001_add_accrued_data_to_savings_account.xml create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseIntegrationTest.java create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseSavingsIntegrationTest.java create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/savings/accrual/SavingsAccrualAccountingTest.java diff --git a/build.gradle b/build.gradle index aae4ce7f7b..e93485f277 100644 --- a/build.gradle +++ b/build.gradle @@ -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 id 'org.asciidoctor.kindlegen.base' version '3.2.0' apply false id 'com.google.cloud.tools.jib' version '3.4.2' apply false id 'org.sonarqube' version '4.4.1.3373' diff --git a/fineract-avro-schemas/src/main/avro/savings/v1/SavingsAccountSummaryDataV1.avsc b/fineract-avro-schemas/src/main/avro/savings/v1/SavingsAccountSummaryDataV1.avsc index 4516ece423..bba565ac98 100644 --- a/fineract-avro-schemas/src/main/avro/savings/v1/SavingsAccountSummaryDataV1.avsc +++ b/fineract-avro-schemas/src/main/avro/savings/v1/SavingsAccountSummaryDataV1.avsc @@ -138,6 +138,22 @@ "null", "string" ] + }, + { + "default": null, + "name": "accruedTillDate", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "totalInterestAccrued", + "type": [ + "null", + "bigdecimal" + ] } ] } diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobName.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobName.java index 678f4858e7..41044300e1 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobName.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobName.java @@ -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("Add Periodic Accrual Transactions for Savings"); // ; private final String name; diff --git a/fineract-provider/src/main/java/org/apache/fineract/organisation/staff/domain/StaffRepository.java b/fineract-core/src/main/java/org/apache/fineract/organisation/staff/domain/StaffRepository.java similarity index 100% rename from fineract-provider/src/main/java/org/apache/fineract/organisation/staff/domain/StaffRepository.java rename to fineract-core/src/main/java/org/apache/fineract/organisation/staff/domain/StaffRepository.java diff --git a/fineract-provider/src/main/java/org/apache/fineract/organisation/staff/domain/StaffRepositoryWrapper.java b/fineract-core/src/main/java/org/apache/fineract/organisation/staff/domain/StaffRepositoryWrapper.java similarity index 100% rename from fineract-provider/src/main/java/org/apache/fineract/organisation/staff/domain/StaffRepositoryWrapper.java rename to fineract-core/src/main/java/org/apache/fineract/organisation/staff/domain/StaffRepositoryWrapper.java diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/ClientRepositoryWrapper.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/client/domain/ClientRepositoryWrapper.java similarity index 100% rename from fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/ClientRepositoryWrapper.java rename to fineract-core/src/main/java/org/apache/fineract/portfolio/client/domain/ClientRepositoryWrapper.java diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/domain/GroupRepository.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/group/domain/GroupRepository.java similarity index 100% rename from fineract-provider/src/main/java/org/apache/fineract/portfolio/group/domain/GroupRepository.java rename to fineract-core/src/main/java/org/apache/fineract/portfolio/group/domain/GroupRepository.java diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/domain/GroupRepositoryWrapper.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/group/domain/GroupRepositoryWrapper.java similarity index 100% rename from fineract-provider/src/main/java/org/apache/fineract/portfolio/group/domain/GroupRepositoryWrapper.java rename to fineract-core/src/main/java/org/apache/fineract/portfolio/group/domain/GroupRepositoryWrapper.java diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/exception/CenterNotActiveException.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/group/exception/CenterNotActiveException.java similarity index 100% rename from fineract-provider/src/main/java/org/apache/fineract/portfolio/group/exception/CenterNotActiveException.java rename to fineract-core/src/main/java/org/apache/fineract/portfolio/group/exception/CenterNotActiveException.java diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/exception/GroupNotFoundException.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/group/exception/GroupNotFoundException.java similarity index 100% rename from fineract-provider/src/main/java/org/apache/fineract/portfolio/group/exception/GroupNotFoundException.java rename to fineract-core/src/main/java/org/apache/fineract/portfolio/group/exception/GroupNotFoundException.java diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountSummaryData.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountSummaryData.java index a4be4cde39..7b5ad3f254 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountSummaryData.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountSummaryData.java @@ -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; public SavingsAccountSummaryData(final CurrencyData currency, final BigDecimal totalDeposits, final BigDecimal totalWithdrawals, final BigDecimal totalWithdrawalFees, final BigDecimal totalAnnualFees, final BigDecimal totalInterestEarned, final BigDecimal totalInterestPosted, final BigDecimal accountBalance, final BigDecimal totalFeeCharge, final BigDecimal totalPenaltyCharge, final BigDecimal totalOverdraftInterestDerived, final BigDecimal totalWithholdTax, final BigDecimal interestNotPosted, final LocalDate lastInterestCalculationDate, final BigDecimal availableBalance, - final LocalDate interestPostedTillDate) { + final LocalDate interestPostedTillDate, final LocalDate accruedTillDate) { this.currency = currency; this.totalDeposits = totalDeposits; this.totalWithdrawals = totalWithdrawals; @@ -79,6 +81,7 @@ public SavingsAccountSummaryData(final CurrencyData currency, final BigDecimal t this.lastInterestCalculationDate = lastInterestCalculationDate; this.availableBalance = availableBalance; this.interestPostedTillDate = interestPostedTillDate; + this.accruedTillDate = accruedTillDate; } public void setPrevInterestPostedTillDate(LocalDate interestPostedTillDate) { @@ -255,6 +258,7 @@ public void updateSummary(final CurrencyData currency, final SavingsAccountTrans this.totalPenaltyCharge = wrapper.calculateTotalPenaltyChargeWaived(currency, transactions); this.totalOverdraftInterestDerived = wrapper.calculateTotalOverdraftInterest(currency, transactions); this.totalWithholdTax = wrapper.calculateTotalWithholdTaxWithdrawal(currency, transactions); + this.totalInterestAccrued = wrapper.calculateTotalInterestAccrued(currency, transactions); // boolean isUpdated = false; updateRunningBalanceAndPivotDate(false, transactions, null, null, null); diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionData.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionData.java index 9a06951b51..b1d7f39f78 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionData.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionData.java @@ -626,6 +626,10 @@ public boolean isWithHoldTaxAndNotReversed() { return SavingsAccountTransactionType.fromInt(this.transactionType.getId().intValue()).isWithHoldTax() && isNotReversed(); } + public boolean isAccrual() { + return this.transactionType.isAccrual(); + } + public boolean isNotReversed() { return !isReversed(); } diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransactionDataSummaryWrapper.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransactionDataSummaryWrapper.java index 2655c256a1..ad454a5152 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransactionDataSummaryWrapper.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransactionDataSummaryWrapper.java @@ -142,4 +142,15 @@ public BigDecimal calculateTotalWithholdTaxWithdrawal(CurrencyData currency, Lis } return total.getAmountDefaultedToNullIfZero(); } + + public BigDecimal calculateTotalInterestAccrued(CurrencyData currency, List transactions) { + Money total = Money.zero(currency); + for (final SavingsAccountTransactionData transaction : transactions) { + if (transaction.isAccrual() && !transaction.isReversalTransaction()) { + total = total.plus(transaction.getAmount()); + } + } + return total.getAmountDefaultedToNullIfZero(); + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java index 62bd6564e7..f206401010 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java @@ -203,12 +203,13 @@ public SavingsDTO populateSavingsDtoFromMap(final Map accounting if (map.containsKey("savingsChargesPaid")) { @SuppressWarnings("unchecked") final List> savingsChargesPaidData = (List>) map.get("savingsChargesPaid"); - for (final Map loanChargePaid : savingsChargesPaidData) { - final Long chargeId = (Long) loanChargePaid.get("chargeId"); - final Long loanChargeId = (Long) loanChargePaid.get("savingsChargeId"); - final boolean isPenalty = (Boolean) loanChargePaid.get("isPenalty"); - final BigDecimal chargeAmountPaid = (BigDecimal) loanChargePaid.get("amount"); + for (final Map savingsChargesPaid : savingsChargesPaidData) { + final Long chargeId = (Long) savingsChargesPaid.get("chargeId"); + final Long loanChargeId = (Long) savingsChargesPaid.get("savingsChargeId"); + final boolean isPenalty = (Boolean) savingsChargesPaid.get("isPenalty"); + final BigDecimal chargeAmountPaid = (BigDecimal) savingsChargesPaid.get("amount"); final ChargePaymentDTO chargePaymentDTO = new ChargePaymentDTO(chargeId, chargeAmountPaid, loanChargeId); + if (isPenalty) { penaltyPayments.add(chargePaymentDTO); } else { @@ -602,6 +603,21 @@ public void createAccrualBasedJournalEntriesAndReversalsForSavingsTax(final Offi savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); } + public void createAccrualBasedJournalEntriesAndReversalsForSavings(final Office office, final String currencyCode, + final Integer accountTypeToBeDebited, final Integer accountTypeToBeCredited, final Long savingsProductId, + final Long paymentTypeId, final Long loanId, final String transactionId, final LocalDate transactionDate, + final BigDecimal amount, final Boolean isReversal) { + int accountTypeToDebitId = accountTypeToBeDebited; + int accountTypeToCreditId = accountTypeToBeCredited; + // reverse debits and credits for reversals + if (isReversal) { + accountTypeToDebitId = accountTypeToBeCredited; + accountTypeToCreditId = accountTypeToBeDebited; + } + createJournalEntriesForSavings(office, currencyCode, accountTypeToDebitId, accountTypeToCreditId, savingsProductId, paymentTypeId, + loanId, transactionId, transactionDate, amount); + } + public void createCashBasedDebitJournalEntriesAndReversalsForSavings(final Office office, final String currencyCode, final Integer accountTypeToBeDebited, final Long savingsProductId, final Long paymentTypeId, final Long savingsId, final String transactionId, final LocalDate transactionDate, final BigDecimal amount, final Boolean isReversal) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForSavings.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForSavings.java index 4aa1b935bd..647c24c149 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForSavings.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForSavings.java @@ -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, + this.helper.createAccrualBasedJournalEntriesAndReversalsForSavings(office, currencyCode, AccrualAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), FinancialActivity.LIABILITY_TRANSFER.getValue(), savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, overdraftAmount, isReversal); if (isPositive) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + this.helper.createAccrualBasedJournalEntriesAndReversalsForSavings(office, currencyCode, AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), FinancialActivity.LIABILITY_TRANSFER.getValue(), savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount.subtract(overdraftAmount), isReversal); } } else { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + this.helper.createAccrualBasedJournalEntriesAndReversalsForSavings(office, currencyCode, AccrualAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), AccrualAccountsForSavings.SAVINGS_REFERENCE.getValue(), savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, overdraftAmount, isReversal); if (isPositive) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + this.helper.createAccrualBasedJournalEntriesAndReversalsForSavings(office, currencyCode, AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), AccrualAccountsForSavings.SAVINGS_REFERENCE.getValue(), savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount.subtract(overdraftAmount), isReversal); @@ -86,23 +86,23 @@ public void createJournalEntriesForSavings(final SavingsDTO savingsDTO) { else if (savingsTransactionDTO.getTransactionType().isDeposit() && savingsTransactionDTO.isOverdraftTransaction()) { boolean isPositive = amount.subtract(overdraftAmount).compareTo(BigDecimal.ZERO) > 0; if (savingsTransactionDTO.isAccountTransfer()) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + this.helper.createAccrualBasedJournalEntriesAndReversalsForSavings(office, currencyCode, FinancialActivity.LIABILITY_TRANSFER.getValue(), AccrualAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, overdraftAmount, isReversal); if (isPositive) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + this.helper.createAccrualBasedJournalEntriesAndReversalsForSavings(office, currencyCode, FinancialActivity.LIABILITY_TRANSFER.getValue(), AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount.subtract(overdraftAmount), isReversal); } } else { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + this.helper.createAccrualBasedJournalEntriesAndReversalsForSavings(office, currencyCode, AccrualAccountsForSavings.SAVINGS_REFERENCE.getValue(), AccrualAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, overdraftAmount, isReversal); if (isPositive) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + this.helper.createAccrualBasedJournalEntriesAndReversalsForSavings(office, currencyCode, AccrualAccountsForSavings.SAVINGS_REFERENCE.getValue(), AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount.subtract(overdraftAmount), isReversal); @@ -113,11 +113,11 @@ else if (savingsTransactionDTO.getTransactionType().isDeposit() && savingsTransa /** Handle Deposits and reversals of deposits **/ else if (savingsTransactionDTO.getTransactionType().isDeposit()) { if (savingsTransactionDTO.isAccountTransfer()) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + this.helper.createAccrualBasedJournalEntriesAndReversalsForSavings(office, currencyCode, FinancialActivity.LIABILITY_TRANSFER.getValue(), AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); } else { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + this.helper.createAccrualBasedJournalEntriesAndReversalsForSavings(office, currencyCode, AccrualAccountsForSavings.SAVINGS_REFERENCE.getValue(), AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); } @@ -125,7 +125,7 @@ else if (savingsTransactionDTO.getTransactionType().isDeposit()) { /** Handle Deposits and reversals of Dividend pay outs **/ else if (savingsTransactionDTO.getTransactionType().isDividendPayout()) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + this.helper.createAccrualBasedJournalEntriesAndReversalsForSavings(office, currencyCode, FinancialActivity.PAYABLE_DIVIDENDS.getValue(), AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); } @@ -133,18 +133,18 @@ else if (savingsTransactionDTO.getTransactionType().isDividendPayout()) { /** Handle withdrawals and reversals of withdrawals **/ else if (savingsTransactionDTO.getTransactionType().isWithdrawal()) { if (savingsTransactionDTO.isAccountTransfer()) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + this.helper.createAccrualBasedJournalEntriesAndReversalsForSavings(office, currencyCode, AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), FinancialActivity.LIABILITY_TRANSFER.getValue(), savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); } else { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + this.helper.createAccrualBasedJournalEntriesAndReversalsForSavings(office, currencyCode, AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), AccrualAccountsForSavings.SAVINGS_REFERENCE.getValue(), savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); } } else if (savingsTransactionDTO.getTransactionType().isEscheat()) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + this.helper.createAccrualBasedJournalEntriesAndReversalsForSavings(office, currencyCode, AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), AccrualAccountsForSavings.ESCHEAT_LIABILITY.getValue(), savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); } @@ -156,12 +156,12 @@ else if (savingsTransactionDTO.getTransactionType().isInterestPosting() && savin // Post journal entry if earned interest amount is greater than // zero if (savingsTransactionDTO.getAmount().compareTo(BigDecimal.ZERO) > 0) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + this.helper.createAccrualBasedJournalEntriesAndReversalsForSavings(office, currencyCode, AccrualAccountsForSavings.INTEREST_ON_SAVINGS.getValue(), AccrualAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, overdraftAmount, isReversal); if (isPositive) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + this.helper.createAccrualBasedJournalEntriesAndReversalsForSavings(office, currencyCode, AccrualAccountsForSavings.INTEREST_ON_SAVINGS.getValue(), AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount.subtract(overdraftAmount), isReversal); @@ -173,7 +173,7 @@ else if (savingsTransactionDTO.getTransactionType().isInterestPosting()) { // Post journal entry if earned interest amount is greater than // zero if (savingsTransactionDTO.getAmount().compareTo(BigDecimal.ZERO) > 0) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + this.helper.createAccrualBasedJournalEntriesAndReversalsForSavings(office, currencyCode, AccrualAccountsForSavings.INTEREST_PAYABLE.getValue(), AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); } @@ -182,9 +182,16 @@ else if (savingsTransactionDTO.getTransactionType().isInterestPosting()) { else if (savingsTransactionDTO.getTransactionType().isAccrual()) { // Post journal entry for Accrual Recognition if (savingsTransactionDTO.getAmount().compareTo(BigDecimal.ZERO) > 0) { - 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) { + this.helper.createAccrualBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + AccrualAccountsForSavings.FEES_RECEIVABLE.getValue(), AccrualAccountsForSavings.INCOME_FROM_FEES.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); + } else { + this.helper.createAccrualBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + AccrualAccountsForSavings.INTEREST_ON_SAVINGS.getValue(), + AccrualAccountsForSavings.INTEREST_PAYABLE.getValue(), savingsProductId, paymentTypeId, savingsId, + transactionId, transactionDate, amount, isReversal); + } } } @@ -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; // Is the Charge a penalty? if (penaltyPayments.size() > 0) { this.helper.createAccrualBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, @@ -206,9 +214,8 @@ else if (savingsTransactionDTO.getTransactionType().isFeeDeduction() && savingsT penaltyPayments); if (isPositive) { this.helper.createAccrualBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, - AccrualAccountsForSavings.SAVINGS_CONTROL, AccrualAccountsForSavings.INCOME_FROM_PENALTIES, - savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, - amount.subtract(overdraftAmount), isReversal, penaltyPayments); + accountTypeToBeDebited, AccrualAccountsForSavings.INCOME_FROM_PENALTIES, savingsProductId, paymentTypeId, + savingsId, transactionId, transactionDate, amount.subtract(overdraftAmount), isReversal, penaltyPayments); } } else { this.helper.createAccrualBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, @@ -217,29 +224,29 @@ else if (savingsTransactionDTO.getTransactionType().isFeeDeduction() && savingsT feePayments); if (isPositive) { this.helper.createAccrualBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, - AccrualAccountsForSavings.SAVINGS_CONTROL, AccrualAccountsForSavings.INCOME_FROM_FEES, savingsProductId, - paymentTypeId, savingsId, transactionId, transactionDate, amount.subtract(overdraftAmount), isReversal, - feePayments); + accountTypeToBeDebited, AccrualAccountsForSavings.INCOME_FROM_FEES, savingsProductId, paymentTypeId, + savingsId, transactionId, transactionDate, amount.subtract(overdraftAmount), isReversal, feePayments); } } } else if (savingsTransactionDTO.getTransactionType().isFeeDeduction()) { + AccrualAccountsForSavings accountTypeToBeCredited = AccrualAccountsForSavings.INCOME_FROM_PENALTIES; // Is the Charge a penalty? if (penaltyPayments.size() > 0) { this.helper.createAccrualBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, - AccrualAccountsForSavings.SAVINGS_CONTROL, AccrualAccountsForSavings.INCOME_FROM_PENALTIES, savingsProductId, - paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal, penaltyPayments); + AccrualAccountsForSavings.SAVINGS_CONTROL, accountTypeToBeCredited, savingsProductId, paymentTypeId, savingsId, + transactionId, transactionDate, amount, isReversal, penaltyPayments); } else { this.helper.createAccrualBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, - AccrualAccountsForSavings.SAVINGS_CONTROL, AccrualAccountsForSavings.INCOME_FROM_FEES, savingsProductId, - paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal, feePayments); + AccrualAccountsForSavings.SAVINGS_CONTROL, accountTypeToBeCredited, savingsProductId, paymentTypeId, savingsId, + transactionId, transactionDate, amount, isReversal, feePayments); } } /** Handle Transfers proposal **/ else if (savingsTransactionDTO.getTransactionType().isInitiateTransfer()) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + this.helper.createAccrualBasedJournalEntriesAndReversalsForSavings(office, currencyCode, AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), AccrualAccountsForSavings.TRANSFERS_SUSPENSE.getValue(), savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); } @@ -247,18 +254,18 @@ else if (savingsTransactionDTO.getTransactionType().isInitiateTransfer()) { /** Handle Transfer Withdrawal or Acceptance **/ else if (savingsTransactionDTO.getTransactionType().isWithdrawTransfer() || savingsTransactionDTO.getTransactionType().isApproveTransfer()) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + this.helper.createAccrualBasedJournalEntriesAndReversalsForSavings(office, currencyCode, 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, 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, AccrualAccountsForSavings.LOSSES_WRITTEN_OFF.getValue(), AccrualAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountsApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountsApiResourceSwagger.java index 520d443223..0bdd796d4f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountsApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountsApiResourceSwagger.java @@ -21,7 +21,9 @@ import io.swagger.v3.oas.annotations.media.Schema; import java.math.BigDecimal; import java.time.LocalDate; +import java.util.List; import java.util.Set; +import org.apache.fineract.portfolio.savings.api.SavingsProductsApiResourceSwagger.PostSavingsProductsRequest.PostSavingsCharges; /** * Created by Chirag Gupta on 12/29/17. @@ -233,6 +235,33 @@ private PostSavingsAccountsRequest() {} public String submittedOnDate; @Schema(example = "123") public String externalId; + @Schema(example = "5.0") + public Double nominalAnnualInterestRate; + @Schema(example = "1") + public Integer interestCompoundingPeriodType; + @Schema(example = "4") + public Integer interestPostingPeriodType; + @Schema(example = "1") + public Integer interestCalculationType; + @Schema(example = "365") + public Integer interestCalculationDaysInYearType; + @Schema(example = "accountMappingForPayment") + public String accountMappingForPayment; + @Schema(example = "false") + public boolean withdrawalFeeForTransfers; + @Schema(example = "false") + public boolean enforceMinRequiredBalance; + @Schema(example = "false") + public boolean isDormancyTrackingActive; + @Schema(example = "false") + public boolean allowOverdraft; + @Schema(example = "false") + public boolean withHoldTax; + @Schema(example = "1") + public Integer lockinPeriodFrequencyType; + @Schema(example = "4") + public Integer lockinPeriodFrequency; + public Set charges; } @Schema(description = "PostSavingsAccountsResponse") @@ -264,6 +293,218 @@ private GetSavingsAccountsSummary() {} public BigDecimal accountBalance; @Schema(example = "0") public BigDecimal availableBalance; + @Schema(example = "0") + public BigDecimal interestNotPosted; + @Schema(example = "[2013, 11, 1]") + public LocalDate lastInterestCalculationDate; + @Schema(example = "0") + public BigDecimal totalDeposits; + @Schema(example = "0") + public BigDecimal totalInterestEarned; + @Schema(example = "0") + public BigDecimal totalInterestPosted; + @Schema(example = "0") + public BigDecimal totalOverdraftInterestDerived; + @Schema(example = "[2013, 11, 1]") + public LocalDate accruedTillDate; + @Schema(example = "0") + public BigDecimal totalInterestAccrued; + } + + static final class GetPaymentType { + + private GetPaymentType() {} + + @Schema(example = "11") + public Long id; + @Schema(example = "Cash") + public String name; + @Schema(example = "Cash Payment") + public String description; + @Schema(example = "true") + public Boolean isCashPayment; + @Schema(example = "0") + public Long position; + } + + static final class GetSavingsAccountsTransaction { + + private GetSavingsAccountsTransaction() {} + + static final class GetLoansLoanIdLoanTransactionEnumData { + + private GetLoansLoanIdLoanTransactionEnumData() {} + + @Schema(example = "1") + public Long id; + @Schema(example = "loanTransactionType.repayment") + public String code; + @Schema(example = "2") + public Long accountId; + @Schema(example = "000000002") + public String accountNo; + + @Schema(example = "false") + public boolean disbursement; + @Schema(example = "false") + public boolean repaymentAtDisbursement; + @Schema(example = "true") + public boolean repayment; + @Schema(example = "false") + public boolean merchantIssuedRefund; + @Schema(example = "false") + public boolean payoutRefund; + @Schema(example = "false") + public boolean goodwillCredit; + @Schema(example = "false") + public boolean contra; + @Schema(example = "false") + public boolean waiveInterest; + @Schema(example = "false") + public boolean waiveCharges; + @Schema(example = "false") + public boolean accrual; + @Schema(example = "false") + public boolean writeOff; + @Schema(example = "false") + public boolean recoveryRepayment; + @Schema(example = "false") + public boolean initiateTransfer; + @Schema(example = "false") + public boolean approveTransfer; + @Schema(example = "false") + public boolean withdrawTransfer; + @Schema(example = "false") + public boolean rejectTransfer; + @Schema(example = "false") + public boolean chargePayment; + @Schema(example = "false") + public boolean refund; + @Schema(example = "false") + public boolean refundForActiveLoans; + @Schema(example = "false") + public boolean creditBalanceRefund; + @Schema(example = "false") + public boolean chargeAdjustment; + @Schema(example = "false") + public boolean chargeoff; + } + + static final class GetPaymentDetailData { + + private GetPaymentDetailData() {} + + @Schema(example = "62") + public Long id; + public GetPaymentType paymentType; + @Schema(example = "acc123") + public String accountNumber; + @Schema(example = "che123") + public String checkNumber; + @Schema(example = "rou123") + public String routingCode; + @Schema(example = "rec123") + public String receiptNumber; + @Schema(example = "ban123") + public String bankNumber; + } + + static final class GetSavingsAccountsTransactionChargePaidByData { + + private GetSavingsAccountsTransactionChargePaidByData() {} + + @Schema(example = "11") + public Long id; + @Schema(example = "100.000000") + public Double amount; + @Schema(example = "9679") + public Integer installmentNumber; + @Schema(example = "1") + public Long chargeId; + @Schema(example = "636") + public Long transactionId; + @Schema(example = "name") + public String name; + } + + static final class GetTranscationTypeEnumData { + + private GetTranscationTypeEnumData() {} + + @Schema(example = "1") + public Long id; + @Schema(example = "savingsAccountTransactionType.deposit") + public String code; + @Schema(example = "Deposit") + public String value; + @Schema(example = "true") + public boolean accrual; + @Schema(example = "true") + public boolean deposit; + @Schema(example = "false") + public boolean dividendPayout; + @Schema(example = "false") + public boolean withdrawal; + @Schema(example = "false") + public boolean interestPosting; + @Schema(example = "false") + public boolean feeDeduction; + @Schema(example = "false") + public boolean initiateTransfer; + @Schema(example = "false") + public boolean approveTransfer; + @Schema(example = "false") + public boolean withdrawTransfer; + @Schema(example = "false") + public boolean rejectTransfer; + @Schema(example = "false") + public boolean overdraftInterest; + @Schema(example = "false") + public boolean writtenoff; + @Schema(example = "true") + public boolean overdraftFee; + @Schema(example = "false") + public boolean withholdTax; + @Schema(example = "false") + public boolean escheat; + @Schema(example = "false") + public boolean amountHold; + @Schema(example = "false") + public boolean amountRelease; + } + + @Schema(example = "1") + public Long id; + @Schema(example = "100.000000") + public BigDecimal amount; + @Schema(description = "List of SavingsAccountsTransactionChargePaidByData") + public List chargesPaidByData; + @Schema(description = "Currency") + public GetSavingsAccountsResponse.GetSavingsPageItems.GetSavingsCurrency currency; + @Schema(example = "[2022, 07, 01]") + public LocalDate date; + @Schema(example = "false") + public boolean interestedPostedAsOn; + @Schema(example = "false") + public boolean isManualTransaction; + @Schema(example = "false") + public boolean isReversal; + @Schema(example = "false") + public boolean lienTransaction; + @Schema(example = "1") + public Long originalTransactionId; + @Schema(example = "1") + public Long releaseTransactionId; + @Schema(example = "false") + public boolean reversed; + @Schema(example = "100.000000") + public BigDecimal runningBalance; + @Schema(example = "[2022, 07, 01]") + public LocalDate submittedOnDate; + @Schema(example = "admin") + public String submittedByUsername; + @Schema(description = "Transaction type") + public GetTranscationTypeEnumData transactionType; } @Schema(example = "1") @@ -280,6 +521,10 @@ private GetSavingsAccountsSummary() {} public String savingsProductName; @Schema(example = "0") public Integer fieldOfficerId; + @Schema(example = "false") + public boolean withHoldTax; + @Schema(example = "false") + public boolean withdrawalFeeForTransfers; public GetSavingsAccountsResponse.GetSavingsPageItems.GetSavingsStatus status; public GetSavingsAccountsResponse.GetSavingsPageItems.GetSavingsTimeline timeline; public GetSavingsAccountsResponse.GetSavingsPageItems.GetSavingsCurrency currency; @@ -290,6 +535,8 @@ private GetSavingsAccountsSummary() {} public GetSavingsAccountsResponse.GetSavingsPageItems.GetSavingsInterestCalculationType interestCalculationType; public GetSavingsAccountsResponse.GetSavingsPageItems.GetSavingsInterestCalculationDaysInYearType interestCalculationDaysInYearType; public GetSavingsAccountsSummary summary; + @Schema(description = "Set of SavingsAccountsTransaction") + public List transactions; } @Schema(description = "PutSavingsAccountsAccountIdRequest") diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsProductsApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsProductsApiResourceSwagger.java index f2fe2ef698..81399958e9 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsProductsApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsProductsApiResourceSwagger.java @@ -67,11 +67,46 @@ private PostSavingsCharges() {} public Integer interestCalculationType; @Schema(example = "365") public Integer interestCalculationDaysInYearType; + @Schema(example = "accountMappingForPayment") + public String accountMappingForPayment; + @Schema(example = "false") + public boolean withdrawalFeeForTransfers; + @Schema(example = "false") + public boolean enforceMinRequiredBalance; + @Schema(example = "false") + public boolean isDormancyTrackingActive; + @Schema(example = "false") + public boolean allowOverdraft; + @Schema(example = "false") + public boolean withHoldTax; @Schema(example = "1") public Integer accountingRule; + @Schema(example = "5") + public Long savingsReferenceAccountId; + @Schema(example = "5") + public Long overdraftPortfolioControlId; + @Schema(example = "5") + public Long feesReceivableAccountId; + @Schema(example = "5") + public Long penaltiesReceivableAccountId; + @Schema(example = "5") + public Long savingsControlAccountId; + @Schema(example = "5") + public Long transfersInSuspenseAccountId; + @Schema(example = "5") + public Long interestPayableAccountId; + @Schema(example = "5") + public Long incomeFromFeeAccountId; + @Schema(example = "5") + public Long incomeFromPenaltyAccountId; + @Schema(example = "5") + public Long incomeFromInterestId; + @Schema(example = "5") + public Long interestOnSavingsAccountId; + @Schema(example = "5") + public Long writeOffAccountId; + public Set charges; - @Schema(example = "accountMappingForPayment") - public String accountMappingForPayment; } @Schema(description = "PostSavingsProductsResponse") diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositAccount.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositAccount.java index 2855bc1e56..fde99a7901 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositAccount.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositAccount.java @@ -149,7 +149,7 @@ public void modifyApplication(final JsonCommand command, final Map extractData(final ResultSet rs) throws SQLExcept interestNotPosted = totalInterestEarned.subtract(totalInterestPosted).add(totalOverdraftInterestDerived); lastInterestCalculationDate = JdbcSupport.getLocalDate(rs, "lastInterestCalculationDate"); } + final LocalDate accruedTillDate = JdbcSupport.getLocalDate(rs, "accruedTillDate"); final SavingsAccountSummaryData summary = new SavingsAccountSummaryData(currency, totalDeposits, totalWithdrawals, totalWithdrawalFees, totalAnnualFees, totalInterestEarned, totalInterestPosted, accountBalance, totalFeeCharge, totalPenaltyCharge, totalOverdraftInterestDerived, totalWithholdTax, interestNotPosted, - lastInterestCalculationDate, availableBalance, interestPostedTillDate); + lastInterestCalculationDate, availableBalance, interestPostedTillDate, accruedTillDate); summary.setPrevInterestPostedTillDate(interestPostedTillDate); final boolean withHoldTax = rs.getBoolean("withHoldTax"); @@ -766,6 +772,7 @@ private static final class SavingAccountMapper implements RowMapper getAccountsIdsByStatusPaged(Integer status, int pageSize, Long public Long retrieveAccountIdByExternalId(final ExternalId externalId) { return savingsAccountRepositoryWrapper.findIdByExternalId(externalId); } + + @Override + public Collection retrievePeriodicAccrualData(LocalDate tillDate, SavingsAccount savings) { + final SavingAccrualMapper mapper = new SavingAccrualMapper(); + final StringBuilder sqlBuilder = new StringBuilder(400); + sqlBuilder.append(" select " + mapper.schema() + " where "); + + sqlBuilder.append(" savings.status_enum = ? "); + sqlBuilder.append(" and (savings.nominal_annual_interest_rate is not null and savings.nominal_annual_interest_rate > 0)"); + sqlBuilder.append(" and msp.accounting_type = ?"); + sqlBuilder.append(" and (savings.closedon_date is null or savings.closedon_date <= ?)"); + sqlBuilder.append(" and (savings.accrued_till_date is null or savings.accrued_till_date <= ?) "); + if (savings != null) { + sqlBuilder.append(" and savings.id = " + savings.getId()); + } + sqlBuilder.append(" order by savings.id "); + try { + return this.jdbcTemplate.query(sqlBuilder.toString(), mapper, new Object[] { SavingsAccountStatusType.ACTIVE.getValue(), + AccountingRuleType.ACCRUAL_PERIODIC.getValue(), tillDate, tillDate }); + } catch (EmptyResultDataAccessException e) { + return new ArrayList<>(); + } + } + + private static final class SavingAccrualMapper implements RowMapper { + + private final String schemaSql; + + SavingAccrualMapper() { + final StringBuilder sqlBuilder = new StringBuilder(400); + sqlBuilder.append( + " savings.id as savingsId, savings.status_enum as status, (CASE WHEN savings.client_id is null THEN mg.office_id ELSE mc.office_id END) as officeId, "); + sqlBuilder.append( + " savings.accrued_till_date as accruedTill, savings.product_id as productId, savings.deposit_type_enum as depositType, "); + sqlBuilder.append(" savings.account_no as accountNo, savings.nominal_annual_interest_rate as nominalAnnualIterestRate, "); + sqlBuilder.append(" savings.interest_compounding_period_enum as interestCompoundingPeriodType, "); + sqlBuilder.append(" savings.interest_posting_period_enum as interestPostingPeriodType, "); + sqlBuilder.append(" savings.interest_calculation_type_enum as interestCalculationType, "); + sqlBuilder.append(" savings.interest_calculation_days_in_year_type_enum as interestCalculationDaysInYearType, "); + sqlBuilder.append(" savings.min_balance_for_interest_calculation as minBalanceForInterestCalculation, "); + sqlBuilder.append(" savings.interest_posted_till_date as postedTill, tg.id as taxGroupId, "); + sqlBuilder.append( + " savings.currency_code as currencyCode, savings.currency_digits as currencyDigits, savings.currency_multiplesof as inMultiplesOf, "); + sqlBuilder.append( + " curr.display_symbol as currencyDisplaySymbol,curr.name as currencyName,curr.internationalized_name_code as currencyNameCode "); + sqlBuilder.append(" from m_savings_account savings "); + sqlBuilder.append(" left join m_savings_product msp on msp.id = savings.product_id "); + sqlBuilder.append(" left join m_client mc on mc.id = savings.client_id "); + sqlBuilder.append(" left join m_group mg on mg.id = savings.group_id "); + sqlBuilder.append(" left join m_currency curr on curr.code = savings.currency_code "); + sqlBuilder.append(" left join m_tax_group tg on tg.id = savings.tax_group_id "); + + this.schemaSql = sqlBuilder.toString(); + } + + public String schema() { + return this.schemaSql; + } + + @Override + public SavingsAccrualData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { + final Long savingsId = rs.getLong("savingsId"); + final String accountNo = rs.getString("accountNo"); + final Long productId = rs.getLong("productId"); + final Long officeId = rs.getLong("officeId"); + final LocalDate accruedTill = JdbcSupport.getLocalDate(rs, "accruedTill"); + final LocalDate postedTill = JdbcSupport.getLocalDate(rs, "postedTill"); + final Integer depositTypeId = rs.getInt("depositType"); + final EnumOptionData depositType = SavingsEnumerations.depositType(depositTypeId); + + final String currencyCode = rs.getString("currencyCode"); + final String currencyName = rs.getString("currencyName"); + final String currencyNameCode = rs.getString("currencyNameCode"); + final String currencyDisplaySymbol = rs.getString("currencyDisplaySymbol"); + final Integer currencyDigits = JdbcSupport.getInteger(rs, "currencyDigits"); + final Integer inMultiplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf"); + final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMultiplesOf, currencyDisplaySymbol, + currencyNameCode); + + final BigDecimal nominalAnnualIterestRate = rs.getBigDecimal("nominalAnnualIterestRate"); + + final EnumOptionData interestCompoundingPeriodType = SavingsEnumerations.compoundingInterestPeriodType( + SavingsCompoundingInterestPeriodType.fromInt(JdbcSupport.getInteger(rs, "interestCompoundingPeriodType"))); + + final EnumOptionData interestPostingPeriodType = SavingsEnumerations.interestPostingPeriodType( + SavingsPostingInterestPeriodType.fromInt(JdbcSupport.getInteger(rs, "interestPostingPeriodType"))); + + final EnumOptionData interestCalculationType = SavingsEnumerations + .interestCalculationType(SavingsInterestCalculationType.fromInt(JdbcSupport.getInteger(rs, "interestCalculationType"))); + + final EnumOptionData interestCalculationDaysInYearType = SavingsEnumerations.interestCalculationDaysInYearType( + SavingsInterestCalculationDaysInYearType.fromInt(JdbcSupport.getInteger(rs, "interestCalculationDaysInYearType"))); + + return new SavingsAccrualData(savingsId, accountNo, depositType, null, productId, officeId, accruedTill, postedTill, currency, + nominalAnnualIterestRate, interestCompoundingPeriodType, interestPostingPeriodType, interestCalculationType, + interestCalculationDaysInYearType, BigDecimal.ZERO); + } + } + } diff --git a/fineract-savings/dependencies.gradle b/fineract-savings/dependencies.gradle index c4dd82a0d8..c8fa6d60ae 100644 --- a/fineract-savings/dependencies.gradle +++ b/fineract-savings/dependencies.gradle @@ -33,6 +33,8 @@ dependencies { implementation( 'org.springframework.boot:spring-boot-starter-web', 'org.springframework.boot:spring-boot-starter-security', + 'org.springframework.boot:spring-boot-starter-batch', + 'org.springframework.batch:spring-batch-integration', 'jakarta.ws.rs:jakarta.ws.rs-api', 'org.glassfish.jersey.media:jersey-media-multipart', diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccrualData.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccrualData.java new file mode 100644 index 0000000000..9da50c0b5c --- /dev/null +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccrualData.java @@ -0,0 +1,52 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.savings.data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.data.EnumOptionData; +import org.apache.fineract.organisation.monetary.data.CurrencyData; +import org.apache.fineract.portfolio.tax.data.TaxGroupData; + +@Data +@RequiredArgsConstructor +public class SavingsAccrualData { + + private final Long id; + private final String accountNo; + private final EnumOptionData depositType; + private final SavingsAccountStatusEnumData status; + private final Long savingsProductId; + private final Long officeId; + private final LocalDate accruedTill; + private final LocalDate postedTill; + private final CurrencyData currencyData; + private final BigDecimal nominalAnnualInterestRate; + private final EnumOptionData interestCompoundingPeriodType; + private final EnumOptionData interestPostingPeriodType; + private final EnumOptionData interestCalculationType; + private final EnumOptionData interestCalculationDaysInYearType; + + private final BigDecimal accruedInterestIncome; + private LocalDate interestCalculatedFrom; + private TaxGroupData taxGroup; + +} diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java index bfd94a1ead..c6b1f15d1a 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java @@ -926,8 +926,7 @@ private BigDecimal getEffectiveOverdraftInterestRateAsFraction(MathContext mc) { return this.nominalAnnualInterestRateOverdraft.divide(BigDecimal.valueOf(100L), mc); } - @SuppressWarnings("unused") - protected BigDecimal getEffectiveInterestRateAsFraction(final MathContext mc, final LocalDate upToInterestCalculationDate) { + public BigDecimal getEffectiveInterestRateAsFraction(final MathContext mc, final LocalDate upToInterestCalculationDate) { return this.nominalAnnualInterestRate.divide(BigDecimal.valueOf(100L), mc); } @@ -3740,6 +3739,20 @@ public LocalDate retrieveLastTransactionDateWithPivotConfig() { return lastransactionDate; } + public List retreiveOrderedAccrualTransactions() { + final List listOfTransactionsSorted = retrieveListOfTransactions(); + + final List orderedAccrualTransactions = new ArrayList<>(); + + for (final SavingsAccountTransaction transaction : listOfTransactionsSorted) { + if (transaction.isAccrual()) { + orderedAccrualTransactions.add(transaction); + } + } + orderedAccrualTransactions.sort(new SavingsAccountTransactionComparator()); + return orderedAccrualTransactions; + } + public BigDecimal getSavingsHoldAmount() { return this.savingsOnHoldAmount == null ? BigDecimal.ZERO : this.savingsOnHoldAmount; } @@ -3842,4 +3855,11 @@ public List toSavingsAccountTr .map(transaction -> transaction.toSavingsAccountTransactionDetailsForPostingPeriod(this.currency, this.allowOverdraft)) .toList(); } + + public List toSavingsAccountTransactionDetailsForPostingPeriodList() { + return retreiveOrderedNonInterestPostingTransactions().stream() + .map(transaction -> transaction.toSavingsAccountTransactionDetailsForPostingPeriod(this.currency, this.allowOverdraft)) + .toList(); + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountAssembler.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountAssembler.java similarity index 98% rename from fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountAssembler.java rename to fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountAssembler.java index 0fd16e6dbb..51c239e83c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountAssembler.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountAssembler.java @@ -78,16 +78,12 @@ import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData; import org.apache.fineract.portfolio.savings.exception.SavingsProductNotFoundException; import org.apache.fineract.useradministration.domain.AppUser; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; @Service public class SavingsAccountAssembler { - private static final Logger LOG = LoggerFactory.getLogger(SavingsAccountAssembler.class); private final SavingsAccountTransactionSummaryWrapper savingsAccountTransactionSummaryWrapper; private final SavingsAccountTransactionDataSummaryWrapper savingsAccountTransactionDataSummaryWrapper; private final SavingsHelper savingsHelper; @@ -98,7 +94,6 @@ public class SavingsAccountAssembler { private final SavingsAccountRepositoryWrapper savingsAccountRepository; private final SavingsAccountChargeAssembler savingsAccountChargeAssembler; private final FromJsonHelper fromApiJsonHelper; - private final JdbcTemplate jdbcTemplate; private final ConfigurationDomainService configurationDomainService; private final ExternalIdFactory externalIdFactory; @@ -109,7 +104,7 @@ public SavingsAccountAssembler(final SavingsAccountTransactionSummaryWrapper sav final StaffRepositoryWrapper staffRepository, final SavingsProductRepository savingProductRepository, final SavingsAccountRepositoryWrapper savingsAccountRepository, final SavingsAccountChargeAssembler savingsAccountChargeAssembler, final FromJsonHelper fromApiJsonHelper, - final AccountTransfersReadPlatformService accountTransfersReadPlatformService, final JdbcTemplate jdbcTemplate, + final AccountTransfersReadPlatformService accountTransfersReadPlatformService, final ConfigurationDomainService configurationDomainService, ExternalIdFactory externalIdFactory) { this.savingsAccountTransactionSummaryWrapper = savingsAccountTransactionSummaryWrapper; this.savingsAccountTransactionDataSummaryWrapper = savingsAccountTransactionDataSummaryWrapper; @@ -121,7 +116,6 @@ public SavingsAccountAssembler(final SavingsAccountTransactionSummaryWrapper sav this.savingsAccountChargeAssembler = savingsAccountChargeAssembler; this.fromApiJsonHelper = fromApiJsonHelper; savingsHelper = new SavingsHelper(accountTransfersReadPlatformService); - this.jdbcTemplate = jdbcTemplate; this.configurationDomainService = configurationDomainService; this.externalIdFactory = externalIdFactory; } @@ -363,8 +357,8 @@ public SavingsAccount loadTransactionsToSavingsAccount(final SavingsAccount acco .findTransactionRunningBalanceBeforePivotDate(account, interestPostedTillDate.minusDays(relaxingDaysForPivotDate + 1)); if (pivotDateTransaction != null && !pivotDateTransaction.isEmpty()) { - account.getSummary().setRunningBalanceOnPivotDate(pivotDateTransaction.get(pivotDateTransaction.size() - 1) - .getRunningBalance(account.getCurrency()).getAmount()); + account.getSummary().setRunningBalanceOnInterestPostingTillDate(pivotDateTransaction + .get(pivotDateTransaction.size() - 1).getRunningBalance(account.getCurrency()).getAmount()); } } else { savingsAccountTransactions = this.savingsAccountRepository.findTransactionsAfterPivotDate(account, diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountSummary.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountSummary.java index 03880fb606..4269e08bd6 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountSummary.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountSummary.java @@ -25,6 +25,8 @@ import java.time.LocalDate; import java.util.HashMap; import java.util.List; +import lombok.Getter; +import lombok.Setter; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; @@ -33,6 +35,8 @@ /** * {@link SavingsAccountSummary} encapsulates all the summary details of a {@link SavingsAccount}. */ +@Getter +@Setter @Embeddable public final class SavingsAccountSummary { @@ -83,6 +87,12 @@ public final class SavingsAccountSummary { @Column(name = "interest_posted_till_date") private LocalDate interestPostedTillDate; + @Column(name = "accrued_till_date") + protected LocalDate accruedTillDate; + + @Column(name = "total_interest_accrued_derived", scale = 6, precision = 19, nullable = true) + private BigDecimal totalInterestAccrued; + @Transient private BigDecimal runningBalanceOnInterestPostingTillDate = BigDecimal.ZERO; @@ -271,71 +281,8 @@ public Money getAccountBalance(final MonetaryCurrency currency) { return Money.of(currency, this.accountBalance); } - public BigDecimal getAccountBalance() { - return this.accountBalance; - } - - public void setAccountBalance(BigDecimal accountBalance) { - this.accountBalance = accountBalance; - } - - public BigDecimal getTotalInterestPosted() { - return this.totalInterestPosted; - } - - public LocalDate getLastInterestCalculationDate() { - return this.lastInterestCalculationDate; - } - - public void setInterestPostedTillDate(final LocalDate date) { - this.interestPostedTillDate = date; - } - - public LocalDate getInterestPostedTillDate() { - return this.interestPostedTillDate; - } - - public void setRunningBalanceOnPivotDate(final BigDecimal runningBalanceOnPivotDate) { - this.runningBalanceOnInterestPostingTillDate = runningBalanceOnPivotDate; - } - public BigDecimal getRunningBalanceOnPivotDate() { return this.runningBalanceOnInterestPostingTillDate; } - public BigDecimal getTotalWithdrawals() { - return this.totalWithdrawals; - } - - public BigDecimal getTotalDeposits() { - return this.totalDeposits; - } - - public BigDecimal getTotalWithdrawalFees() { - return this.totalWithdrawalFees; - } - - public BigDecimal getTotalFeeCharge() { - return this.totalFeeCharge; - } - - public BigDecimal getTotalPenaltyCharge() { - return this.totalPenaltyCharge; - } - - public BigDecimal getTotalAnnualFees() { - return this.totalAnnualFees; - } - - public BigDecimal getTotalInterestEarned() { - return this.totalInterestEarned; - } - - public BigDecimal getTotalOverdraftInterestDerived() { - return this.totalOverdraftInterestDerived; - } - - public BigDecimal getTotalWithholdTax() { - return this.totalWithholdTax; - } } diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransaction.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransaction.java index 96aef54284..37bc520bbb 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransaction.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransaction.java @@ -41,6 +41,7 @@ import java.util.Optional; import java.util.Set; import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; +import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.domain.LocalDateInterval; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; @@ -337,6 +338,15 @@ public static SavingsAccountTransaction releaseAmount(SavingsAccountTransaction accountTransaction.refNo); } + public static SavingsAccountTransaction accrual(final SavingsAccount savingsAccount, final Office office, final LocalDate date, + final Money amount, final boolean isManualTransaction) { + final boolean isReversed = false; + final Boolean lienTransaction = false; + final String refNo = ExternalId.generate().getValue(); + return new SavingsAccountTransaction(savingsAccount, office, SavingsAccountTransactionType.ACCRUAL.getValue(), date, amount, + isReversed, isManualTransaction, lienTransaction, refNo); + } + public static SavingsAccountTransaction reversal(SavingsAccountTransaction accountTransaction) { SavingsAccountTransaction sat = copyTransaction(accountTransaction); sat.reversed = false; @@ -559,6 +569,10 @@ public boolean isTransferRelatedTransaction() { return isTransferInitiation() || isTransferApproval() || isTransferRejection() || isTransferWithdrawal(); } + public boolean isAccrual() { + return getTransactionType().isAccrual(); + } + public void zeroBalanceFields() { this.runningBalance = null; this.cumulativeBalance = null; diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/jobs/addaccrualtransactionforsavings/AddAccrualTransactionForSavingsConfig.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/jobs/addaccrualtransactionforsavings/AddAccrualTransactionForSavingsConfig.java new file mode 100644 index 0000000000..537260bfb2 --- /dev/null +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/jobs/addaccrualtransactionforsavings/AddAccrualTransactionForSavingsConfig.java @@ -0,0 +1,60 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.savings.jobs.addaccrualtransactionforsavings; + +import org.apache.fineract.infrastructure.jobs.service.JobName; +import org.apache.fineract.portfolio.savings.service.accrual.SavingsAccrualDomainServiceImpl; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.launch.support.RunIdIncrementer; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.PlatformTransactionManager; + +@Configuration +public class AddAccrualTransactionForSavingsConfig { + + @Autowired + private JobRepository jobRepository; + @Autowired + private PlatformTransactionManager transactionManager; + @Autowired + private SavingsAccrualDomainServiceImpl savingsAccrualDomainService; + + @Bean + protected Step addAccrualTransactionForSavingsStep() { + return new StepBuilder(JobName.ADD_PERIODIC_ACCRUAL_ENTRIES_FOR_SAVINGS.name(), jobRepository) + .tasklet(addAccrualTransactionForSavingsTasklet(), transactionManager).build(); + } + + @Bean + public Job addAccrualTransactionForSavingsJob() { + return new JobBuilder(JobName.ADD_PERIODIC_ACCRUAL_ENTRIES_FOR_SAVINGS.name(), jobRepository) + .start(addAccrualTransactionForSavingsStep()).incrementer(new RunIdIncrementer()).build(); + } + + @Bean + public AddAccrualTransactionForSavingsTasklet addAccrualTransactionForSavingsTasklet() { + return new AddAccrualTransactionForSavingsTasklet(savingsAccrualDomainService); + } +} diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/jobs/addaccrualtransactionforsavings/AddAccrualTransactionForSavingsTasklet.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/jobs/addaccrualtransactionforsavings/AddAccrualTransactionForSavingsTasklet.java new file mode 100644 index 0000000000..d5904862b7 --- /dev/null +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/jobs/addaccrualtransactionforsavings/AddAccrualTransactionForSavingsTasklet.java @@ -0,0 +1,50 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.savings.jobs.addaccrualtransactionforsavings; + +import java.time.LocalDate; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.exception.MultiException; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException; +import org.apache.fineract.portfolio.savings.service.accrual.SavingsAccrualDomainServiceImpl; +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.batch.repeat.RepeatStatus; + +@RequiredArgsConstructor +public class AddAccrualTransactionForSavingsTasklet implements Tasklet { + + private final SavingsAccrualDomainServiceImpl savingsAccrualDomainService; + + @Override + public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { + try { + addPeriodicAccruals(DateUtils.getBusinessLocalDate()); + } catch (MultiException e) { + throw new JobExecutionException(e); + } + return RepeatStatus.FINISHED; + } + + private void addPeriodicAccruals(final LocalDate tilldate) throws MultiException { + savingsAccrualDomainService.addAccrualEntries(tilldate); + } +} diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformService.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformService.java index 4a39f44540..42d7e08a05 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformService.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformService.java @@ -27,6 +27,8 @@ import org.apache.fineract.portfolio.savings.DepositAccountType; import org.apache.fineract.portfolio.savings.data.SavingsAccountData; import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData; +import org.apache.fineract.portfolio.savings.data.SavingsAccrualData; +import org.apache.fineract.portfolio.savings.domain.SavingsAccount; public interface SavingsAccountReadPlatformService { @@ -71,4 +73,7 @@ List retrieveAllSavingsDataForInterestPosting(boolean backda List retrieveAllTransactionData(List refNo); Long retrieveAccountIdByExternalId(ExternalId externalId); + + Collection retrievePeriodicAccrualData(LocalDate tillDate, SavingsAccount savings); + } diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/accrual/SavingsAccrualDomainServiceImpl.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/accrual/SavingsAccrualDomainServiceImpl.java new file mode 100644 index 0000000000..a510dad924 --- /dev/null +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/accrual/SavingsAccrualDomainServiceImpl.java @@ -0,0 +1,196 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.savings.service.accrual; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; +import org.apache.fineract.infrastructure.core.domain.LocalDateInterval; +import org.apache.fineract.infrastructure.core.service.MathUtil; +import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.organisation.monetary.domain.MoneyHelper; +import org.apache.fineract.portfolio.savings.SavingsCompoundingInterestPeriodType; +import org.apache.fineract.portfolio.savings.SavingsInterestCalculationDaysInYearType; +import org.apache.fineract.portfolio.savings.SavingsInterestCalculationType; +import org.apache.fineract.portfolio.savings.SavingsPostingInterestPeriodType; +import org.apache.fineract.portfolio.savings.data.SavingsAccrualData; +import org.apache.fineract.portfolio.savings.domain.SavingsAccount; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountCharge; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountChargePaidBy; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction; +import org.apache.fineract.portfolio.savings.domain.SavingsHelper; +import org.apache.fineract.portfolio.savings.domain.interest.CompoundInterestValues; +import org.apache.fineract.portfolio.savings.domain.interest.PostingPeriod; +import org.apache.fineract.portfolio.savings.domain.interest.SavingsAccountTransactionDetailsForPostingPeriod; +import org.apache.fineract.portfolio.savings.service.SavingsAccountDomainService; +import org.apache.fineract.portfolio.savings.service.SavingsAccountReadPlatformService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@Transactional +@RequiredArgsConstructor +public class SavingsAccrualDomainServiceImpl { + + private final SavingsAccountReadPlatformService savingsAccountReadPlatformService; + private final SavingsAccountAssembler savingsAccountAssembler; + private final SavingsAccountRepositoryWrapper savingsAccountRepository; + private final SavingsHelper savingsHelper; + private final ConfigurationDomainService configurationDomainService; + private final SavingsAccountDomainService savingsAccountDomainService; + + public void addAccrualEntries(LocalDate tillDate) throws JobExecutionException { + final Collection savingsAccrualData = savingsAccountReadPlatformService.retrievePeriodicAccrualData(tillDate, + null); + log.debug("Savings Accrual for date {} : {}", tillDate, savingsAccrualData.size()); + final Integer financialYearBeginningMonth = configurationDomainService.retrieveFinancialYearBeginningMonth(); + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final MathContext mc = MoneyHelper.getMathContext(); + + List errors = new ArrayList<>(); + for (SavingsAccrualData savingsAccrual : savingsAccrualData) { + try { + SavingsAccount savingsAccount = savingsAccountAssembler.assembleFrom(savingsAccrual.getId(), false); + LocalDate fromDate = savingsAccrual.getAccruedTill(); + if (fromDate == null) { + fromDate = savingsAccount.getActivationDate(); + } + log.debug("Processing savings account {} from date {} till date {}", savingsAccrual.getAccountNo(), fromDate, tillDate); + addAccrualTransactions(savingsAccount, fromDate, tillDate, financialYearBeginningMonth, + isSavingsInterestPostingAtCurrentPeriodEnd, mc); + } catch (Exception e) { + log.error("Failed to add accrual transaction for savings {} : {}", savingsAccrual.getAccountNo(), e.getMessage()); + errors.add(e.getCause()); + } + } + if (!errors.isEmpty()) { + throw new JobExecutionException(errors); + } + } + + public boolean isChargeToBeRecognizedAsAccrual(final Collection chargeIds, final SavingsAccountCharge savingsAccountCharge) { + if (chargeIds.isEmpty()) { + return false; + } + return chargeIds.contains(savingsAccountCharge.getCharge().getId()); + } + + public SavingsAccountTransaction addSavingsChargeAccrualTransaction(SavingsAccount savingsAccount, + SavingsAccountCharge savingsAccountCharge, LocalDate transactionDate) { + final MonetaryCurrency currency = savingsAccount.getCurrency(); + final Money chargeAmount = savingsAccountCharge.getAmount(currency); + SavingsAccountTransaction savingsAccountTransaction = SavingsAccountTransaction.accrual(savingsAccount, savingsAccount.office(), + transactionDate, chargeAmount, false); + final SavingsAccountChargePaidBy chargePaidBy = SavingsAccountChargePaidBy.instance(savingsAccountTransaction, savingsAccountCharge, + savingsAccountTransaction.getAmount(currency).getAmount()); + savingsAccountTransaction.getSavingsAccountChargesPaid().add(chargePaidBy); + + savingsAccount.addTransaction(savingsAccountTransaction); + return savingsAccountTransaction; + } + + private void addAccrualTransactions(SavingsAccount savingsAccount, final LocalDate fromDate, final LocalDate tillDate, + final Integer financialYearBeginningMonth, final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final MathContext mc) { + + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + existingTransactionIds.addAll(savingsAccount.findExistingTransactionIds()); + existingReversedTransactionIds.addAll(savingsAccount.findExistingReversedTransactionIds()); + + List postedAsOnTransactionDates = savingsAccount.getManualPostingDates(); + final SavingsPostingInterestPeriodType postingPeriodType = SavingsPostingInterestPeriodType + .fromInt(savingsAccount.getInterestCompoundingPeriodType()); + + final SavingsCompoundingInterestPeriodType compoundingPeriodType = SavingsCompoundingInterestPeriodType + .fromInt(savingsAccount.getInterestCompoundingPeriodType()); + + final SavingsInterestCalculationDaysInYearType daysInYearType = SavingsInterestCalculationDaysInYearType + .fromInt(savingsAccount.getInterestCalculationDaysInYearType()); + + final List postingPeriodIntervals = this.savingsHelper.determineInterestPostingPeriods(fromDate, tillDate, + postingPeriodType, financialYearBeginningMonth, postedAsOnTransactionDates); + + final List allPostingPeriods = new ArrayList<>(); + final MonetaryCurrency currency = savingsAccount.getCurrency(); + Money periodStartingBalance = Money.zero(currency); + + final SavingsInterestCalculationType interestCalculationType = SavingsInterestCalculationType + .fromInt(savingsAccount.getInterestCalculationType()); + final BigDecimal interestRateAsFraction = savingsAccount.getEffectiveInterestRateAsFraction(mc, tillDate); + final Collection interestPostTransactions = this.savingsHelper.fetchPostInterestTransactionIds(savingsAccount.getId()); + boolean isInterestTransfer = false; + final Money minBalanceForInterestCalculation = Money.of(currency, savingsAccount.getMinBalanceForInterestCalculation()); + List savingsAccountTransactionDetailsForPostingPeriodList = savingsAccount + .toSavingsAccountTransactionDetailsForPostingPeriodList(); + for (final LocalDateInterval periodInterval : postingPeriodIntervals) { + final boolean isUserPosting = (postedAsOnTransactionDates.contains(periodInterval.endDate())); + + final PostingPeriod postingPeriod = PostingPeriod.createFrom(periodInterval, periodStartingBalance, + savingsAccountTransactionDetailsForPostingPeriodList, currency, compoundingPeriodType, interestCalculationType, + interestRateAsFraction, daysInYearType.getValue(), tillDate, interestPostTransactions, isInterestTransfer, + minBalanceForInterestCalculation, isSavingsInterestPostingAtCurrentPeriodEnd, isUserPosting, + financialYearBeginningMonth); + + periodStartingBalance = postingPeriod.closingBalance(); + + allPostingPeriods.add(postingPeriod); + } + BigDecimal compoundedInterest = BigDecimal.ZERO; + BigDecimal unCompoundedInterest = BigDecimal.ZERO; + final CompoundInterestValues compoundInterestValues = new CompoundInterestValues(compoundedInterest, unCompoundedInterest); + + final List accrualTransactionDates = savingsAccount.retreiveOrderedAccrualTransactions().stream() + .map(transaction -> transaction.getTransactionDate()).toList(); + + LocalDate accruedTillDate = fromDate; + for (PostingPeriod period : allPostingPeriods) { + if (MathUtil.isGreaterThanZero(period.closingBalance())) { + period.calculateInterest(compoundInterestValues); + log.debug(" period {} {} : {}", period.getPeriodInterval().startDate(), period.getPeriodInterval().endDate(), + period.getInterestEarned()); + if (!accrualTransactionDates.contains(period.getPeriodInterval().endDate())) { + SavingsAccountTransaction savingsAccountTransaction = SavingsAccountTransaction.accrual(savingsAccount, + savingsAccount.office(), period.getPeriodInterval().endDate(), period.getInterestEarned(), false); + savingsAccount.addTransaction(savingsAccountTransaction); + } + } + accruedTillDate = period.getPeriodInterval().endDate(); + } + + savingsAccount.getSummary().setAccruedTillDate(accruedTillDate); + savingsAccountRepository.saveAndFlush(savingsAccount); + + savingsAccountDomainService.postJournalEntries(savingsAccount, existingTransactionIds, existingReversedTransactionIds, false); + } + +} diff --git a/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/module-changelog-master.xml b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/module-changelog-master.xml similarity index 92% rename from fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/module-changelog-master.xml rename to fineract-savings/src/main/resources/db/changelog/tenant/module/savings/module-changelog-master.xml index 8746633220..f00f111d4b 100644 --- a/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/module-changelog-master.xml +++ b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/module-changelog-master.xml @@ -23,4 +23,5 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd"> + diff --git a/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/0001_add_accrued_data_to_savings_account.xml b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/0001_add_accrued_data_to_savings_account.xml new file mode 100644 index 0000000000..85e376d2c2 --- /dev/null +++ b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/0001_add_accrued_data_to_savings_account.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + select count(*) from job where name='Add Periodic Accrual Transactions for Savings' + + + + + + + + select count(*) from job where name='Add Periodic Accrual Transactions for Savings' + + + + + + + + + + + + + + + + + + + + + + + select count(*) from m_permission where entity_name='PERIODICACCRUALACCOUNTINGFORSAVINGS' + + + + + + + + + + diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseIntegrationTest.java new file mode 100644 index 0000000000..acae6b580f --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseIntegrationTest.java @@ -0,0 +1,101 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.integrationtests; + +import static java.lang.Boolean.FALSE; +import static java.lang.Boolean.TRUE; +import static org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType.BUSINESS_DATE; + +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.builder.ResponseSpecBuilder; +import io.restassured.http.ContentType; +import io.restassured.internal.RequestSpecificationImpl; +import io.restassured.specification.RequestSpecification; +import io.restassured.specification.ResponseSpecification; +import org.apache.fineract.client.models.BusinessDateRequest; +import org.apache.fineract.integrationtests.common.BusinessDateHelper; +import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper; +import org.apache.fineract.integrationtests.common.Utils; +import org.apache.fineract.integrationtests.useradministration.users.UserHelper; +import org.hamcrest.Matcher; +import org.hamcrest.Matchers; + +public abstract class BaseIntegrationTest { + + protected static final String DATETIME_PATTERN = "dd MMMM yyyy"; + protected static final String MONTH_DATE_PATTERN = "dd MMMM"; + protected static final String LOCALE = "en_GB"; + + static { + Utils.initializeRESTAssured(); + } + + private final String fullAdminAuthKey = getFullAdminAuthKey(); + + protected BusinessDateHelper businessDateHelper = new BusinessDateHelper(); + protected final ResponseSpecification responseSpec = createResponseSpecification(Matchers.is(200)); + protected final RequestSpecification requestSpec = createRequestSpecification(fullAdminAuthKey); + + private final String nonByPassUserAuthKey = getNonByPassUserAuthKey(requestSpec, responseSpec); + + protected void runAt(String date, Runnable runnable) { + try { + GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(requestSpec, responseSpec, 42, true); + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, TRUE); + businessDateHelper.updateBusinessDate( + new BusinessDateRequest().type(BUSINESS_DATE.getName()).date(date).dateFormat(DATETIME_PATTERN).locale("en")); + runnable.run(); + } finally { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, FALSE); + GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(requestSpec, responseSpec, 42, false); + } + } + + protected void runAsNonByPass(Runnable runnable) { + RequestSpecificationImpl requestSpecImpl = (RequestSpecificationImpl) requestSpec; + try { + requestSpecImpl.replaceHeader("Authorization", "Basic " + nonByPassUserAuthKey); + runnable.run(); + } finally { + requestSpecImpl.replaceHeader("Authorization", "Basic " + fullAdminAuthKey); + } + } + + protected RequestSpecification createRequestSpecification(String authKey) { + RequestSpecification requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); + requestSpec.header("Authorization", "Basic " + authKey); + requestSpec.header("Fineract-Platform-TenantId", "default"); + return requestSpec; + } + + protected ResponseSpecification createResponseSpecification(Matcher statusCodeMatcher) { + return new ResponseSpecBuilder().expectStatusCode(statusCodeMatcher).build(); + } + + protected String getFullAdminAuthKey() { + return Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey(); + } + + protected String getNonByPassUserAuthKey(RequestSpecification requestSpec, ResponseSpecification responseSpec) { + // creates the user + UserHelper.getSimpleUserWithoutBypassPermission(requestSpec, responseSpec); + return Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey(UserHelper.SIMPLE_USER_NAME, UserHelper.SIMPLE_USER_PASSWORD); + } + +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java index 359be4c55c..09c6bbcdc0 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java @@ -18,8 +18,6 @@ */ package org.apache.fineract.integrationtests; -import static java.lang.Boolean.FALSE; -import static java.lang.Boolean.TRUE; import static org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType.BUSINESS_DATE; import static org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder.DUE_PENALTY_INTEREST_PRINCIPAL_FEE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE_STRATEGY; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -28,10 +26,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import io.restassured.builder.RequestSpecBuilder; import io.restassured.builder.ResponseSpecBuilder; -import io.restassured.http.ContentType; -import io.restassured.internal.RequestSpecificationImpl; import io.restassured.specification.RequestSpecification; import io.restassured.specification.ResponseSpecification; import java.math.BigDecimal; @@ -74,7 +69,6 @@ import org.apache.fineract.integrationtests.common.BatchHelper; import org.apache.fineract.integrationtests.common.BusinessDateHelper; import org.apache.fineract.integrationtests.common.ClientHelper; -import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper; import org.apache.fineract.integrationtests.common.SchedulerJobHelper; import org.apache.fineract.integrationtests.common.Utils; import org.apache.fineract.integrationtests.common.accounting.Account; @@ -89,19 +83,17 @@ import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper; import org.apache.fineract.integrationtests.common.system.CodeHelper; import org.apache.fineract.integrationtests.inlinecob.InlineLoanCOBHelper; -import org.apache.fineract.integrationtests.useradministration.users.UserHelper; import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType; -import org.hamcrest.Matcher; import org.hamcrest.Matchers; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.extension.ExtendWith; @ExtendWith(LoanTestLifecycleExtension.class) -public abstract class BaseLoanIntegrationTest { +public abstract class BaseLoanIntegrationTest extends BaseIntegrationTest { protected static final String DATETIME_PATTERN = "dd MMMM yyyy"; @@ -195,16 +187,6 @@ protected static void validateRepaymentPeriod(GetLoansLoanIdResponse loanDetails assertEquals(paidLate, period.getTotalPaidLateForPeriod()); } - private String getNonByPassUserAuthKey(RequestSpecification requestSpec, ResponseSpecification responseSpec) { - // creates the user - UserHelper.getSimpleUserWithoutBypassPermission(requestSpec, responseSpec); - return Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey(UserHelper.SIMPLE_USER_NAME, UserHelper.SIMPLE_USER_PASSWORD); - } - - private String getFullAdminAuthKey() { - return Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey(); - } - // Loan product with proper accounting setup protected PostLoanProductsRequest createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct() { return new PostLoanProductsRequest().name(Utils.uniqueRandomStringGenerator("LOAN_PRODUCT_", 6))// @@ -363,17 +345,6 @@ protected PostLoanProductsRequest create1InstallmentAmountInMultiplesOf4Period1M .amortizationType(amortizationType); } - private RequestSpecification createRequestSpecification(String authKey) { - RequestSpecification requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); - requestSpec.header("Authorization", "Basic " + authKey); - requestSpec.header("Fineract-Platform-TenantId", "default"); - return requestSpec; - } - - protected ResponseSpecification createResponseSpecification(Matcher statusCodeMatcher) { - return new ResponseSpecBuilder().expectStatusCode(statusCodeMatcher).build(); - } - protected void verifyUndoLastDisbursalShallFail(Long loanId, String expectedError) { ResponseSpecification errorResponse = new ResponseSpecBuilder().expectStatusCode(403).build(); LoanTransactionHelper validationErrorHelper = new LoanTransactionHelper(this.requestSpec, errorResponse); @@ -629,29 +600,6 @@ protected void verifyRepaymentSchedule(Long loanId, Installment... installments) } } - protected void runAt(String date, Runnable runnable) { - try { - GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(requestSpec, responseSpec, 42, true); - GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, TRUE); - businessDateHelper.updateBusinessDate( - new BusinessDateRequest().type(BUSINESS_DATE.getName()).date(date).dateFormat(DATETIME_PATTERN).locale("en")); - runnable.run(); - } finally { - GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, FALSE); - GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(requestSpec, responseSpec, 42, false); - } - } - - protected void runAsNonByPass(Runnable runnable) { - RequestSpecificationImpl requestSpecImpl = (RequestSpecificationImpl) requestSpec; - try { - requestSpecImpl.replaceHeader("Authorization", "Basic " + nonByPassUserAuthKey); - runnable.run(); - } finally { - requestSpecImpl.replaceHeader("Authorization", "Basic " + fullAdminAuthKey); - } - } - protected PostLoansRequest applyLoanRequest(Long clientId, Long loanProductId, String loanDisbursementDate, Double amount, int numberOfRepayments) { return applyLoanRequest(clientId, loanProductId, loanDisbursementDate, amount, numberOfRepayments, null); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseSavingsIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseSavingsIntegrationTest.java new file mode 100644 index 0000000000..3e1674e3fc --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseSavingsIntegrationTest.java @@ -0,0 +1,114 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.integrationtests; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.math.BigDecimal; +import java.util.List; +import org.apache.fineract.client.models.GetSavingsAccountsTransaction; +import org.apache.fineract.client.models.PostSavingsAccountsRequest; +import org.apache.fineract.client.models.PostSavingsProductsRequest; +import org.apache.fineract.integrationtests.common.Utils; +import org.apache.fineract.integrationtests.common.accounting.Account; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class BaseSavingsIntegrationTest extends BaseIntegrationTest { + + private static final Logger LOG = LoggerFactory.getLogger(BaseSavingsIntegrationTest.class); + + static { + Utils.initializeRESTAssured(); + } + + protected PostSavingsProductsRequest createSavingsProductRequest() { + return new PostSavingsProductsRequest().name(Utils.uniqueRandomStringGenerator("SAVINGS_PRODUCT_", 6)) // + .shortName(Utils.uniqueRandomStringGenerator("", 4)) // + .description("Savings Product Description ") // + .currencyCode("USD") // + .digitsAfterDecimal(2) // + .inMultiplesOf(0) // + .nominalAnnualInterestRate(0.0) // + .interestCompoundingPeriodType(1) // + .interestPostingPeriodType(4).interestCalculationType(1) // + .interestCalculationDaysInYearType(365) // + .withdrawalFeeForTransfers(false) // + .enforceMinRequiredBalance(false) // + .isDormancyTrackingActive(false) // + .allowOverdraft(false) // + .withHoldTax(false) // + .accountingRule(1) // + .locale(LOCALE); // + } + + protected PostSavingsAccountsRequest createSavingsAccountRequest(final Integer clientId, final Integer savingsProductId) { + return new PostSavingsAccountsRequest().productId(savingsProductId) // + .clientId(clientId) // + .externalId("") // + .nominalAnnualInterestRate(0.0) // + .interestCompoundingPeriodType(1) // + .interestPostingPeriodType(4).interestCalculationType(1) // + .interestCalculationDaysInYearType(365) // + .withdrawalFeeForTransfers(false) // + .allowOverdraft(false) // + .enforceMinRequiredBalance(false) // + .dateFormat(DATETIME_PATTERN) // + .locale(LOCALE); // + } + + protected PostSavingsProductsRequest createSavingsProductWithAccountMappingForAccrualBased(Double interestRate, + final Account... accountList) { + PostSavingsProductsRequest savingsProductRequest = createSavingsProductRequest().nominalAnnualInterestRate(interestRate) + .accountingRule(3); + for (int i = 0; i < accountList.length; i++) { + if (accountList[i].getAccountType().equals(Account.AccountType.ASSET)) { + final Long ID = accountList[i].getAccountID().longValue(); + savingsProductRequest = savingsProductRequest.savingsReferenceAccountId(ID).overdraftPortfolioControlId(ID) + .feesReceivableAccountId(ID).penaltiesReceivableAccountId(ID); + } + if (accountList[i].getAccountType().equals(Account.AccountType.INCOME)) { + final Long ID = accountList[i].getAccountID().longValue(); + savingsProductRequest = savingsProductRequest.incomeFromFeeAccountId(ID).incomeFromPenaltyAccountId(ID) + .incomeFromInterestId(ID); + } + if (accountList[i].getAccountType().equals(Account.AccountType.EXPENSE)) { + final Long ID = accountList[i].getAccountID().longValue(); + savingsProductRequest = savingsProductRequest.interestOnSavingsAccountId(ID).writeOffAccountId(ID); + } + if (accountList[i].getAccountType().equals(Account.AccountType.LIABILITY)) { + final Long ID = accountList[i].getAccountID().longValue(); + savingsProductRequest = savingsProductRequest.savingsControlAccountId(ID).transfersInSuspenseAccountId(ID) + .interestPayableAccountId(ID); + } + } + + return savingsProductRequest; + } + + protected void checkSavingsAccrualTransactions(final List transactions, final Double amountExpected) { + BigDecimal totalAmount = transactions.stream().filter(transaction -> transaction.getTransactionType().getAccrual() == true) + .map(transaction -> transaction.getAmount()).reduce(BigDecimal.ZERO, BigDecimal::add); + + LOG.info("Savings Accrual transaction amounts are expected %f and total %f".formatted(amountExpected, totalAmount)); + assertEquals(amountExpected, totalAmount.doubleValue(), + "Savings transactions are not equal %f and total %f".formatted(amountExpected, totalAmount)); + } + +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/IntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/IntegrationTest.java index d987a57182..5802356633 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/IntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/IntegrationTest.java @@ -57,6 +57,9 @@ @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public abstract class IntegrationTest { + protected static final String DATETIME_PATTERN = "dd MMMM yyyy"; + protected static final String LOCALE = "en_GB"; + private static final SecureRandom random = new SecureRandom(); private FineractClient fineract; diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsAccountHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsAccountHelper.java index 061f8f8876..5cd97d2988 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsAccountHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsAccountHelper.java @@ -41,7 +41,14 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import org.apache.fineract.client.models.GetSavingsAccountsAccountIdResponse; import org.apache.fineract.client.models.PagedLocalRequestAdvancedQueryRequest; +import org.apache.fineract.client.models.PostSavingsAccountTransactionsRequest; +import org.apache.fineract.client.models.PostSavingsAccountTransactionsResponse; +import org.apache.fineract.client.models.PostSavingsAccountsAccountIdRequest; +import org.apache.fineract.client.models.PostSavingsAccountsAccountIdResponse; +import org.apache.fineract.client.models.PostSavingsAccountsRequest; +import org.apache.fineract.client.models.PostSavingsAccountsResponse; import org.apache.fineract.client.models.SavingsAccountTransactionsSearchResponse; import org.apache.fineract.client.util.JSON; import org.apache.fineract.integrationtests.client.IntegrationTest; @@ -989,4 +996,37 @@ public HashMap getTransactionDetails(Integer savingsId, Integer transactionId) { return Utils.performServerGet(requestSpec, responseSpec, url, ""); } + public PostSavingsAccountsResponse createSavingsAccount(PostSavingsAccountsRequest request) { + return ok(fineract().savingsAccounts.submitApplication2(request)); + } + + public PostSavingsAccountsAccountIdResponse approveSavingsAccount(Long savingsAccountId, String approvedOnDate) { + PostSavingsAccountsAccountIdRequest request = new PostSavingsAccountsAccountIdRequest().approvedOnDate(approvedOnDate) + .dateFormat(DATETIME_PATTERN).locale(LOCALE); + return ok(fineract().savingsAccounts.handleCommands6(savingsAccountId, request, "approve")); + } + + public PostSavingsAccountsAccountIdResponse activateSavingsAccount(Long savingsAccountId, String activatedOnDate) { + PostSavingsAccountsAccountIdRequest request = new PostSavingsAccountsAccountIdRequest().activatedOnDate(activatedOnDate) + .dateFormat(DATETIME_PATTERN).locale(LOCALE); + return ok(fineract().savingsAccounts.handleCommands6(savingsAccountId, request, "activate")); + } + + public GetSavingsAccountsAccountIdResponse getSavingsAccount(Long savingsAccountId) { + return ok(fineract().savingsAccounts.retrieveOne25(savingsAccountId, null, null)); + } + + public PostSavingsAccountTransactionsResponse applySavingsAccountTransaction(Long savingsAccountId, + PostSavingsAccountTransactionsRequest request, String command) { + return ok(fineract().savingsTransactions.transaction2(savingsAccountId, request, command)); + } + + public GetSavingsAccountsAccountIdResponse getSavingsAccount(Long savingsAccountId, Map queryParams) { + final String url = SAVINGS_ACCOUNT_URL + "/" + savingsAccountId; + queryParams.put(TENANT_PARAM_NAME, DEFAULT_TENANT); + requestSpec.queryParams(queryParams); + String response = Utils.performServerGet(this.requestSpec, this.responseSpec, url); + return GSON.fromJson(response, GetSavingsAccountsAccountIdResponse.class); + } + } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsProductHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsProductHelper.java index a22ba29c1e..0a9228e771 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsProductHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsProductHelper.java @@ -27,14 +27,17 @@ import java.util.HashMap; import java.util.Map; import org.apache.fineract.client.models.GetSavingsProductsProductIdResponse; +import org.apache.fineract.client.models.PostSavingsProductsRequest; +import org.apache.fineract.client.models.PostSavingsProductsResponse; import org.apache.fineract.client.util.JSON; +import org.apache.fineract.integrationtests.client.IntegrationTest; import org.apache.fineract.integrationtests.common.Utils; import org.apache.fineract.integrationtests.common.accounting.Account; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @SuppressWarnings("unused") -public class SavingsProductHelper { +public class SavingsProductHelper extends IntegrationTest { private static final Logger LOG = LoggerFactory.getLogger(SavingsProductHelper.class); private static final String SAVINGS_PRODUCT_URL = "/fineract-provider/api/v1/savingsproducts"; @@ -381,4 +384,12 @@ public static GetSavingsProductsProductIdResponse getSavingsProductById(final Re return GSON.fromJson(response, GetSavingsProductsProductIdResponse.class); } + public PostSavingsProductsResponse createSavingsProduct(PostSavingsProductsRequest request) { + return ok(fineract().savingsProducts.create13(request)); + } + + public GetSavingsProductsProductIdResponse getSavingsProduct(Long savingsProductId) { + return ok(fineract().savingsProducts.retrieveOne27(savingsProductId)); + } + } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/savings/accrual/SavingsAccrualAccountingTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/savings/accrual/SavingsAccrualAccountingTest.java new file mode 100644 index 0000000000..916795f1db --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/savings/accrual/SavingsAccrualAccountingTest.java @@ -0,0 +1,190 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.integrationtests.savings.accrual; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.builder.ResponseSpecBuilder; +import io.restassured.http.ContentType; +import io.restassured.specification.RequestSpecification; +import io.restassured.specification.ResponseSpecification; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.HashMap; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicLong; +import org.apache.fineract.client.models.GetSavingsAccountsAccountIdResponse; +import org.apache.fineract.client.models.GetSavingsAccountsTransaction; +import org.apache.fineract.client.models.PostSavingsAccountTransactionsRequest; +import org.apache.fineract.client.models.PostSavingsAccountTransactionsResponse; +import org.apache.fineract.client.models.PostSavingsAccountsRequest; +import org.apache.fineract.client.models.PostSavingsAccountsResponse; +import org.apache.fineract.client.models.PostSavingsProductsRequest; +import org.apache.fineract.client.models.PostSavingsProductsResponse; +import org.apache.fineract.integrationtests.BaseSavingsIntegrationTest; +import org.apache.fineract.integrationtests.common.BusinessDateHelper; +import org.apache.fineract.integrationtests.common.ClientHelper; +import org.apache.fineract.integrationtests.common.SchedulerJobHelper; +import org.apache.fineract.integrationtests.common.Utils; +import org.apache.fineract.integrationtests.common.accounting.Account; +import org.apache.fineract.integrationtests.common.accounting.AccountHelper; +import org.apache.fineract.integrationtests.common.accounting.JournalEntryHelper; +import org.apache.fineract.integrationtests.common.savings.SavingsAccountHelper; +import org.apache.fineract.integrationtests.common.savings.SavingsProductHelper; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SavingsAccrualAccountingTest extends BaseSavingsIntegrationTest { + + private static final Logger LOG = LoggerFactory.getLogger(SavingsAccrualAccountingTest.class); + private static ResponseSpecification responseSpec; + private static RequestSpecification requestSpec; + private static AccountHelper accountHelper; + private static BusinessDateHelper businessDateHelper; + private static ClientHelper clientHelper; + private static SavingsProductHelper savingsProductHelper; + private static SavingsAccountHelper savingsAccountHelper; + private static SchedulerJobHelper schedulerJobHelper; + private static JournalEntryHelper journalEntryHelper; + + @BeforeAll + public static void setup() { + Utils.initializeRESTAssured(); + requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); + requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); + requestSpec.header("Fineract-Platform-TenantId", "default"); + responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build(); + businessDateHelper = new BusinessDateHelper(); + accountHelper = new AccountHelper(requestSpec, responseSpec); + clientHelper = new ClientHelper(requestSpec, responseSpec); + savingsProductHelper = new SavingsProductHelper(); + savingsAccountHelper = new SavingsAccountHelper(requestSpec, responseSpec); + schedulerJobHelper = new SchedulerJobHelper(requestSpec); + journalEntryHelper = new JournalEntryHelper(requestSpec, responseSpec); + } + + // 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 + @Test + public void uc1() { + AtomicLong savingsAccountId = new AtomicLong(); + runAt("1 May 2024", () -> { + final Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + + final Account assetAccount = accountHelper.createAssetAccount(); + final Account incomeAccount = accountHelper.createIncomeAccount(); + final Account expenseAccount = accountHelper.createExpenseAccount(); + final Account overpaymentAccount = accountHelper.createLiabilityAccount(); + + PostSavingsProductsRequest savingsProductRequest = createSavingsProductWithAccountMappingForAccrualBased(1.0, assetAccount, + incomeAccount, expenseAccount, overpaymentAccount); + LOG.info("------------------------------CREATE SAVINGS PRODUCT------------------------------------\n"); + PostSavingsProductsResponse savingsProductResponse = savingsProductHelper.createSavingsProduct(savingsProductRequest); + assertNotNull(savingsProductResponse); + assertNotNull(savingsProductResponse.getResourceId()); + final Integer savingsProductId = savingsProductResponse.getResourceId(); + + PostSavingsAccountsRequest savingsAccountRequest = createSavingsAccountRequest(clientId.intValue(), savingsProductId) + .submittedOnDate("1 May 2024").nominalAnnualInterestRate(2.0); + PostSavingsAccountsResponse savingsAccountResponse = savingsAccountHelper.createSavingsAccount(savingsAccountRequest); + assertNotNull(savingsAccountResponse); + assertNotNull(savingsAccountResponse.getResourceId()); + savingsAccountId.set(savingsAccountResponse.getResourceId().longValue()); + + savingsAccountHelper.approveSavingsAccount(savingsAccountId.get(), "1 May 2024"); + savingsAccountHelper.activateSavingsAccount(savingsAccountId.get(), "1 May 2024"); + + GetSavingsAccountsAccountIdResponse savingsAccountDetails = savingsAccountHelper.getSavingsAccount(savingsAccountId.get()); + LOG.info("Savings account created {}", savingsAccountDetails.getAccountNo()); + assertNotNull(savingsAccountDetails); + assertNotNull(savingsAccountDetails.getId()); + assertTrue(BigDecimal.ZERO.compareTo(savingsAccountDetails.getSummary().getAvailableBalance()) == 0); + + final BigDecimal transactionAmount = BigDecimal.valueOf(10000.0); + PostSavingsAccountTransactionsRequest depositTransactionRequest = new PostSavingsAccountTransactionsRequest() + .transactionDate("1 May 2024").transactionAmount(transactionAmount).paymentTypeId(1).dateFormat(DATETIME_PATTERN) + .locale(LOCALE); + PostSavingsAccountTransactionsResponse transactionResponse = savingsAccountHelper + .applySavingsAccountTransaction(savingsAccountId.get(), depositTransactionRequest, "deposit"); + assertNotNull(transactionResponse); + assertNotNull(transactionResponse.getResourceId()); + + savingsAccountDetails = savingsAccountHelper.getSavingsAccount(savingsAccountId.get()); + LOG.info("Savings account {} with balance {}".formatted(savingsAccountDetails.getAccountNo(), + savingsAccountDetails.getSummary().getAvailableBalance().stripTrailingZeros())); + assertEquals(transactionAmount.stripTrailingZeros(), + savingsAccountDetails.getSummary().getAvailableBalance().stripTrailingZeros()); + }); + + runAt("3 May 2024", () -> { + final HashMap queryParams = new HashMap<>(); + queryParams.put("associations", "all"); + + final BigDecimal transactionAmount = BigDecimal.valueOf(20000.0); + GetSavingsAccountsAccountIdResponse savingsAccountDetails = savingsAccountHelper.getSavingsAccount(savingsAccountId.get()); + PostSavingsAccountTransactionsRequest depositTransactionRequest = new PostSavingsAccountTransactionsRequest() + .transactionDate("3 May 2024").transactionAmount(transactionAmount).paymentTypeId(1).dateFormat(DATETIME_PATTERN) + .locale(LOCALE); + PostSavingsAccountTransactionsResponse transactionResponse = savingsAccountHelper + .applySavingsAccountTransaction(savingsAccountId.get(), depositTransactionRequest, "deposit"); + assertNotNull(transactionResponse); + assertNotNull(transactionResponse.getResourceId()); + + final String jobName = "Add Periodic Accrual Transactions for Savings"; + schedulerJobHelper.executeAndAwaitJob(jobName); + + savingsAccountDetails = savingsAccountHelper.getSavingsAccount(savingsAccountId.get(), queryParams); + LOG.info("Savings account {} {}", savingsAccountDetails.getSummary().getAccruedTillDate(), + savingsAccountDetails.getSummary().getTotalInterestAccrued()); + assertEquals(LocalDate.of(2024, 05, 03), savingsAccountDetails.getSummary().getAccruedTillDate()); + + // Validate Accrual Transactions + final List transactions = savingsAccountDetails.getTransactions(); + checkSavingsAccrualTransactions(transactions, 2.74); + + // Valudate Journal Entries for the Accrual Transactions + final Optional optTransaction = transactions.stream() + .filter(transaction -> transaction.getTransactionType().getAccrual() == true).findFirst(); + assertTrue(optTransaction.isPresent(), "Required Accrual transaction not found"); + final GetSavingsAccountsTransaction accrualTransaction = optTransaction.get(); + final List journalEntries = journalEntryHelper.getJournalEntriesByTransactionId("S" + accrualTransaction.getId()); + assertEquals(2, journalEntries.size()); + assertEquals(accrualTransaction.getAmount().floatValue(), journalEntries.get(0).get("amount")); + assertEquals(accrualTransaction.getAmount().floatValue(), journalEntries.get(1).get("amount")); + }); + } + +}