| /** |
| * 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.domain; |
| |
| import jakarta.persistence.Column; |
| import jakarta.persistence.Embeddable; |
| import jakarta.persistence.Transient; |
| import java.math.BigDecimal; |
| import java.time.LocalDate; |
| import java.util.HashMap; |
| import java.util.List; |
| import org.apache.fineract.infrastructure.core.service.DateUtils; |
| import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; |
| import org.apache.fineract.organisation.monetary.domain.Money; |
| import org.apache.fineract.portfolio.savings.domain.interest.PostingPeriod; |
| |
| /** |
| * {@link SavingsAccountSummary} encapsulates all the summary details of a {@link SavingsAccount}. |
| */ |
| @Embeddable |
| public final class SavingsAccountSummary { |
| |
| @Column(name = "total_deposits_derived", scale = 6, precision = 19) |
| private BigDecimal totalDeposits; |
| |
| @Column(name = "total_withdrawals_derived", scale = 6, precision = 19) |
| private BigDecimal totalWithdrawals; |
| |
| @Column(name = "total_interest_earned_derived", scale = 6, precision = 19) |
| private BigDecimal totalInterestEarned; |
| |
| @Column(name = "total_interest_posted_derived", scale = 6, precision = 19) |
| private BigDecimal totalInterestPosted; |
| |
| @Column(name = "total_withdrawal_fees_derived", scale = 6, precision = 19) |
| private BigDecimal totalWithdrawalFees; |
| |
| @Column(name = "total_fees_charge_derived", scale = 6, precision = 19) |
| private BigDecimal totalFeeCharge; |
| |
| @Column(name = "total_penalty_charge_derived", scale = 6, precision = 19) |
| private BigDecimal totalPenaltyCharge; |
| |
| @Column(name = "total_annual_fees_derived", scale = 6, precision = 19) |
| private BigDecimal totalAnnualFees; |
| |
| @Column(name = "account_balance_derived", scale = 6, precision = 19) |
| private BigDecimal accountBalance = BigDecimal.ZERO; |
| |
| // TODO: AA do we need this data to be persisted. |
| @Transient |
| private BigDecimal totalFeeChargesWaived = BigDecimal.ZERO; |
| |
| @Transient |
| private BigDecimal totalPenaltyChargesWaived = BigDecimal.ZERO; |
| |
| @Column(name = "total_overdraft_interest_derived", scale = 6, precision = 19) |
| private BigDecimal totalOverdraftInterestDerived; |
| |
| @Column(name = "total_withhold_tax_derived", scale = 6, precision = 19) |
| private BigDecimal totalWithholdTax; |
| |
| @Column(name = "last_interest_calculation_date") |
| private LocalDate lastInterestCalculationDate; |
| |
| // Currently this represents the last interest posting date |
| @Column(name = "interest_posted_till_date") |
| private LocalDate interestPostedTillDate; |
| |
| @Transient |
| private BigDecimal runningBalanceOnInterestPostingTillDate = BigDecimal.ZERO; |
| |
| SavingsAccountSummary() { |
| // |
| } |
| |
| public void updateSummary(final MonetaryCurrency currency, final SavingsAccountTransactionSummaryWrapper wrapper, |
| final List<SavingsAccountTransaction> transactions) { |
| |
| this.totalDeposits = wrapper.calculateTotalDeposits(currency, transactions); |
| this.totalWithdrawals = wrapper.calculateTotalWithdrawals(currency, transactions); |
| this.totalInterestPosted = wrapper.calculateTotalInterestPosted(currency, transactions); |
| this.totalWithdrawalFees = wrapper.calculateTotalWithdrawalFees(currency, transactions); |
| this.totalAnnualFees = wrapper.calculateTotalAnnualFees(currency, transactions); |
| this.totalFeeCharge = wrapper.calculateTotalFeesCharge(currency, transactions); |
| this.totalPenaltyCharge = wrapper.calculateTotalPenaltyCharge(currency, transactions); |
| this.totalFeeChargesWaived = wrapper.calculateTotalFeesChargeWaived(currency, transactions); |
| this.totalPenaltyChargesWaived = wrapper.calculateTotalPenaltyChargeWaived(currency, transactions); |
| this.totalOverdraftInterestDerived = wrapper.calculateTotalOverdraftInterest(currency, transactions); |
| this.totalWithholdTax = wrapper.calculateTotalWithholdTaxWithdrawal(currency, transactions); |
| |
| updateRunningBalanceAndPivotDate(false, transactions, null, null, null, currency); |
| |
| this.accountBalance = Money.of(currency, this.totalDeposits).plus(this.totalInterestPosted).minus(this.totalWithdrawals) |
| .minus(this.totalWithdrawalFees).minus(this.totalAnnualFees).minus(this.totalFeeCharge).minus(this.totalPenaltyCharge) |
| .minus(totalOverdraftInterestDerived).minus(totalWithholdTax).getAmount(); |
| } |
| |
| public void updateSummaryWithPivotConfig(final MonetaryCurrency currency, final SavingsAccountTransactionSummaryWrapper wrapper, |
| final SavingsAccountTransaction transaction, final List<SavingsAccountTransaction> savingsAccountTransactions) { |
| |
| if (transaction != null) { |
| if (transaction.isReversalTransaction()) { |
| return; |
| } |
| Money transactionAmount = Money.of(currency, transaction.getAmount()); |
| switch (transaction.getTransactionType()) { |
| case DEPOSIT: |
| if (transaction.isDepositAndNotReversed() || transaction.isDividendPayoutAndNotReversed()) { |
| this.totalDeposits = Money.of(currency, this.totalDeposits).plus(transactionAmount).getAmount(); |
| this.accountBalance = Money.of(currency, this.accountBalance).plus(transactionAmount).getAmount(); |
| } |
| break; |
| case WITHDRAWAL: |
| if (transaction.isWithdrawal() && transaction.isNotReversed()) { |
| this.totalWithdrawals = Money.of(currency, this.totalWithdrawals).plus(transactionAmount).getAmount(); |
| this.accountBalance = Money.of(currency, this.accountBalance).minus(transactionAmount).getAmount(); |
| } |
| break; |
| case WITHDRAWAL_FEE: |
| if (transaction.isWithdrawalFeeAndNotReversed() && transaction.isNotReversed()) { |
| this.totalWithdrawalFees = Money.of(currency, this.totalWithdrawalFees).plus(transactionAmount).getAmount(); |
| this.totalFeeCharge = Money.of(currency, this.totalFeeCharge).plus(transactionAmount).getAmount(); |
| this.accountBalance = Money.of(currency, this.accountBalance).minus(transactionAmount).getAmount(); |
| } |
| break; |
| case ANNUAL_FEE: |
| if (transaction.isAnnualFeeAndNotReversed() && transaction.isNotReversed()) { |
| this.totalAnnualFees = Money.of(currency, this.totalAnnualFees).plus(transactionAmount).getAmount(); |
| this.totalFeeCharge = Money.of(currency, this.totalFeeCharge).plus(transactionAmount).getAmount(); |
| this.accountBalance = Money.of(currency, this.accountBalance).minus(transactionAmount).getAmount(); |
| } |
| break; |
| case WAIVE_CHARGES: |
| if (transaction.isWaiveFeeChargeAndNotReversed()) { |
| this.totalFeeChargesWaived = Money.of(currency, this.totalFeeChargesWaived).plus(transactionAmount.getAmount()) |
| .getAmount(); |
| } else if (transaction.isWaivePenaltyChargeAndNotReversed()) { |
| this.totalPenaltyChargesWaived = Money.of(currency, this.totalPenaltyChargesWaived) |
| .plus(transactionAmount.getAmount()).getAmount(); |
| } |
| break; |
| case PAY_CHARGE: |
| if (transaction.isFeeChargeAndNotReversed()) { |
| this.totalFeeCharge = Money.of(currency, this.totalFeeCharge).plus(transactionAmount).getAmount(); |
| } else if (transaction.isPenaltyChargeAndNotReversed()) { |
| this.totalPenaltyCharge = Money.of(currency, this.totalPenaltyCharge).plus(transactionAmount).getAmount(); |
| } |
| if (transaction.isFeeChargeAndNotReversed() || transaction.isPenaltyChargeAndNotReversed()) { |
| this.accountBalance = Money.of(currency, this.accountBalance).minus(transactionAmount).getAmount(); |
| } |
| break; |
| case OVERDRAFT_INTEREST: |
| if (transaction.isOverdraftInterestAndNotReversed()) { |
| this.totalOverdraftInterestDerived = Money.of(currency, this.totalOverdraftInterestDerived).plus(transactionAmount) |
| .getAmount(); |
| this.accountBalance = Money.of(currency, this.accountBalance).minus(transactionAmount).getAmount(); |
| } |
| break; |
| case WITHHOLD_TAX: |
| if (transaction.isWithHoldTaxAndNotReversed()) { |
| this.totalWithholdTax = Money.of(currency, this.totalWithholdTax).plus(transactionAmount).getAmount(); |
| this.accountBalance = Money.of(currency, this.accountBalance).minus(transactionAmount).getAmount(); |
| } |
| break; |
| default: |
| break; |
| } |
| } else { |
| // INTEREST_POSTING |
| Money interestTotal = Money.zero(currency); |
| Money withHoldTaxTotal = Money.zero(currency); |
| Money overdraftInterestTotal = Money.zero(currency); |
| this.totalDeposits = wrapper.calculateTotalDeposits(currency, savingsAccountTransactions); |
| this.totalWithdrawals = wrapper.calculateTotalWithdrawals(currency, savingsAccountTransactions); |
| |
| final HashMap<String, Money> map = updateRunningBalanceAndPivotDate(true, savingsAccountTransactions, interestTotal, |
| overdraftInterestTotal, withHoldTaxTotal, currency); |
| interestTotal = map.get("interestTotal"); |
| withHoldTaxTotal = map.get("withHoldTax"); |
| overdraftInterestTotal = map.get("overdraftInterestTotal"); |
| this.totalInterestPosted = interestTotal.getAmountDefaultedToNullIfZero(); |
| this.totalOverdraftInterestDerived = overdraftInterestTotal.getAmountDefaultedToNullIfZero(); |
| this.totalWithholdTax = withHoldTaxTotal.getAmountDefaultedToNullIfZero(); |
| |
| this.accountBalance = getRunningBalanceOnPivotDate(); |
| this.accountBalance = Money.of(currency, this.accountBalance).plus(Money.of(currency, this.totalDeposits)) |
| .plus(this.totalInterestPosted).minus(this.totalWithdrawals).minus(this.totalWithholdTax) |
| .minus(this.totalOverdraftInterestDerived).getAmount(); |
| } |
| } |
| |
| @SuppressWarnings("unchecked") |
| private HashMap<String, Money> updateRunningBalanceAndPivotDate(final boolean backdatedTxnsAllowedTill, |
| final List<SavingsAccountTransaction> savingsAccountTransactions, Money interestTotal, Money overdraftInterestTotal, |
| Money withHoldTaxTotal, MonetaryCurrency currency) { |
| boolean isUpdated = false; |
| HashMap<String, Money> map = new HashMap<>(); |
| for (int i = savingsAccountTransactions.size() - 1; i >= 0; i--) { |
| final SavingsAccountTransaction savingsAccountTransaction = savingsAccountTransactions.get(i); |
| if (savingsAccountTransaction.isInterestPostingAndNotReversed() && !savingsAccountTransaction.isReversalTransaction() |
| && !isUpdated) { |
| setInterestPostedTillDate(savingsAccountTransaction.getTransactionDate()); |
| isUpdated = true; |
| if (!backdatedTxnsAllowedTill) { |
| break; |
| } |
| } |
| if (savingsAccountTransaction.isOverdraftInterestAndNotReversed() && !savingsAccountTransaction.isReversalTransaction() |
| && !isUpdated) { |
| setInterestPostedTillDate(savingsAccountTransaction.getTransactionDate()); |
| isUpdated = true; |
| if (!backdatedTxnsAllowedTill) { |
| break; |
| } |
| } |
| if (backdatedTxnsAllowedTill) { |
| if (savingsAccountTransaction.isInterestPostingAndNotReversed() && savingsAccountTransaction.isNotReversed() |
| && !savingsAccountTransaction.isReversalTransaction()) { |
| interestTotal = interestTotal.plus(savingsAccountTransaction.getAmount(currency)); |
| } |
| if (savingsAccountTransaction.isOverdraftInterestAndNotReversed() && !savingsAccountTransaction.isReversalTransaction()) { |
| overdraftInterestTotal = overdraftInterestTotal.plus(savingsAccountTransaction.getAmount()); |
| } |
| if (savingsAccountTransaction.isWithHoldTaxAndNotReversed() && !savingsAccountTransaction.isReversalTransaction()) { |
| withHoldTaxTotal = withHoldTaxTotal.plus(savingsAccountTransaction.getAmount(currency)); |
| } |
| } |
| } |
| if (backdatedTxnsAllowedTill) { |
| map.put("interestTotal", interestTotal); |
| map.put("withHoldTax", withHoldTaxTotal); |
| map.put("overdraftInterestTotal", overdraftInterestTotal); |
| } |
| return map; |
| } |
| |
| public void updateFromInterestPeriodSummaries(final MonetaryCurrency currency, final List<PostingPeriod> allPostingPeriods) { |
| Money totalEarned = Money.zero(currency); |
| for (final PostingPeriod period : allPostingPeriods) { |
| Money interestEarned = period.interest(); |
| interestEarned = interestEarned == null ? Money.zero(currency) : interestEarned; |
| totalEarned = totalEarned.plus(interestEarned); |
| } |
| this.lastInterestCalculationDate = DateUtils.getBusinessLocalDate(); |
| this.totalInterestEarned = totalEarned.getAmount(); |
| } |
| |
| public boolean isLessThanOrEqualToAccountBalance(final Money amount) { |
| final Money accountBalance = getAccountBalance(amount.getCurrency()); |
| return accountBalance.isGreaterThanOrEqualTo(amount); |
| } |
| |
| 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; |
| } |
| } |