Java tutorial
/** * 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 static org.apache.fineract.portfolio.savings.SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.allowOverdraftParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.dueAsOfDateParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.enforceMinRequiredBalanceParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.localeParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.lockinPeriodFrequencyParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.lockinPeriodFrequencyTypeParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.minOverdraftForInterestCalculationParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.minRequiredBalanceParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.nominalAnnualInterestRateOverdraftParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.overdraftLimitParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.withdrawalFeeForTransfersParamName; import java.math.BigDecimal; import java.math.MathContext; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.DiscriminatorColumn; import javax.persistence.DiscriminatorType; import javax.persistence.DiscriminatorValue; import javax.persistence.Embedded; import javax.persistence.Entity; import javax.persistence.Inheritance; import javax.persistence.InheritanceType; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.OneToMany; import javax.persistence.OrderBy; import javax.persistence.Table; import javax.persistence.Temporal; import javax.persistence.TemporalType; import javax.persistence.Transient; import javax.persistence.UniqueConstraint; import javax.persistence.Version; import org.apache.commons.lang.ObjectUtils; import org.apache.commons.lang.StringUtils; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; import org.apache.fineract.infrastructure.core.domain.LocalDateInterval; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.security.service.RandomPasswordGenerator; import org.apache.fineract.organisation.monetary.data.CurrencyData; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.organisation.office.domain.Office; import org.apache.fineract.organisation.staff.domain.Staff; import org.apache.fineract.portfolio.accountdetails.domain.AccountType; import org.apache.fineract.portfolio.charge.domain.Charge; import org.apache.fineract.portfolio.charge.exception.SavingsAccountChargeNotFoundException; import org.apache.fineract.portfolio.client.domain.Client; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.group.domain.Group; import org.apache.fineract.portfolio.savings.DepositAccountType; import org.apache.fineract.portfolio.savings.SavingsApiConstants; 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.SavingsPeriodFrequencyType; import org.apache.fineract.portfolio.savings.SavingsPostingInterestPeriodType; import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionDTO; import org.apache.fineract.portfolio.savings.domain.interest.PostingPeriod; import org.apache.fineract.portfolio.savings.exception.InsufficientAccountBalanceException; import org.apache.fineract.portfolio.savings.exception.SavingsAccountTransactionNotFoundException; import org.apache.fineract.portfolio.savings.exception.SavingsActivityPriorToClientTransferException; import org.apache.fineract.portfolio.savings.exception.SavingsOfficerAssignmentDateException; import org.apache.fineract.portfolio.savings.exception.SavingsOfficerUnassignmentDateException; import org.apache.fineract.portfolio.savings.exception.SavingsTransferTransactionsCannotBeUndoneException; import org.apache.fineract.portfolio.savings.service.SavingsEnumerations; import org.apache.fineract.useradministration.domain.AppUser; import org.hibernate.annotations.LazyCollection; import org.hibernate.annotations.LazyCollectionOption; import org.joda.time.LocalDate; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import org.springframework.data.jpa.domain.AbstractPersistable; import org.springframework.util.CollectionUtils; import com.google.gson.JsonArray; @Entity @Table(name = "m_savings_account", uniqueConstraints = { @UniqueConstraint(columnNames = { "account_no" }, name = "sa_account_no_UNIQUE"), @UniqueConstraint(columnNames = { "external_id" }, name = "sa_external_id_UNIQUE") }) @Inheritance(strategy = InheritanceType.SINGLE_TABLE) @DiscriminatorColumn(name = "deposit_type_enum", discriminatorType = DiscriminatorType.INTEGER) @DiscriminatorValue("100") public class SavingsAccount extends AbstractPersistable<Long> { @Version int version; @Column(name = "account_no", length = 20, unique = true, nullable = false) protected String accountNumber; @Column(name = "external_id", nullable = true) protected String externalId; @ManyToOne(optional = true) @JoinColumn(name = "client_id", nullable = true) protected Client client; @ManyToOne(optional = true) @JoinColumn(name = "group_id", nullable = true) protected Group group; @ManyToOne @JoinColumn(name = "product_id", nullable = false) protected SavingsProduct product; @ManyToOne @JoinColumn(name = "field_officer_id", nullable = true) protected Staff savingsOfficer; @Column(name = "status_enum", nullable = false) protected Integer status; @Column(name = "account_type_enum", nullable = false) protected Integer accountType; @Temporal(TemporalType.DATE) @Column(name = "submittedon_date", nullable = true) protected Date submittedOnDate; @ManyToOne(optional = true) @JoinColumn(name = "submittedon_userid", nullable = true) protected AppUser submittedBy; @Temporal(TemporalType.DATE) @Column(name = "rejectedon_date") protected Date rejectedOnDate; @ManyToOne(optional = true) @JoinColumn(name = "rejectedon_userid", nullable = true) protected AppUser rejectedBy; @Temporal(TemporalType.DATE) @Column(name = "withdrawnon_date") protected Date withdrawnOnDate; @ManyToOne(optional = true) @JoinColumn(name = "withdrawnon_userid", nullable = true) protected AppUser withdrawnBy; @Temporal(TemporalType.DATE) @Column(name = "approvedon_date") protected Date approvedOnDate; @ManyToOne(optional = true) @JoinColumn(name = "approvedon_userid", nullable = true) protected AppUser approvedBy; @Temporal(TemporalType.DATE) @Column(name = "activatedon_date", nullable = true) protected Date activatedOnDate; @ManyToOne(optional = true) @JoinColumn(name = "activatedon_userid", nullable = true) protected AppUser activatedBy; @Temporal(TemporalType.DATE) @Column(name = "closedon_date") protected Date closedOnDate; @ManyToOne(optional = true) @JoinColumn(name = "closedon_userid", nullable = true) protected AppUser closedBy; @Embedded protected MonetaryCurrency currency; @Column(name = "nominal_annual_interest_rate", scale = 6, precision = 19, nullable = false) protected BigDecimal nominalAnnualInterestRate; /** * The interest period is the span of time at the end of which savings in a * client's account earn interest. * * A value from the {@link SavingsCompoundingInterestPeriodType} * enumeration. */ @Column(name = "interest_compounding_period_enum", nullable = false) protected Integer interestCompoundingPeriodType; /** * A value from the {@link SavingsPostingInterestPeriodType} enumeration. */ @Column(name = "interest_posting_period_enum", nullable = false) protected Integer interestPostingPeriodType; /** * A value from the {@link SavingsInterestCalculationType} enumeration. */ @Column(name = "interest_calculation_type_enum", nullable = false) protected Integer interestCalculationType; /** * A value from the {@link SavingsInterestCalculationDaysInYearType} * enumeration. */ @Column(name = "interest_calculation_days_in_year_type_enum", nullable = false) protected Integer interestCalculationDaysInYearType; @Column(name = "min_required_opening_balance", scale = 6, precision = 19, nullable = true) protected BigDecimal minRequiredOpeningBalance; @Column(name = "lockin_period_frequency", nullable = true) protected Integer lockinPeriodFrequency; @Column(name = "lockin_period_frequency_enum", nullable = true) protected Integer lockinPeriodFrequencyType; /** * When account becomes <code>active</code> this field is derived if * <code>lockinPeriodFrequency</code> and * <code>lockinPeriodFrequencyType</code> details are present. */ @Temporal(TemporalType.DATE) @Column(name = "lockedin_until_date_derived", nullable = true) protected Date lockedInUntilDate; @Column(name = "withdrawal_fee_for_transfer", nullable = true) protected boolean withdrawalFeeApplicableForTransfer; @Column(name = "allow_overdraft") private boolean allowOverdraft; @Column(name = "overdraft_limit", scale = 6, precision = 19, nullable = true) private BigDecimal overdraftLimit; @Column(name = "nominal_annual_interest_rate_overdraft", scale = 6, precision = 19, nullable = true) protected BigDecimal nominalAnnualInterestRateOverdraft; @Column(name = "min_overdraft_for_interest_calculation", scale = 6, precision = 19, nullable = true) private BigDecimal minOverdraftForInterestCalculation; @Column(name = "enforce_min_required_balance") private boolean enforceMinRequiredBalance; @Column(name = "min_required_balance", scale = 6, precision = 19, nullable = true) private BigDecimal minRequiredBalance; @Column(name = "on_hold_funds_derived", scale = 6, precision = 19, nullable = true) private BigDecimal onHoldFunds; @Temporal(TemporalType.DATE) @Column(name = "start_interest_calculation_date") protected Date startInterestCalculationDate; @Embedded protected SavingsAccountSummary summary; @OrderBy(value = "dateOf, createdDate, id") @LazyCollection(LazyCollectionOption.FALSE) @OneToMany(cascade = CascadeType.ALL, mappedBy = "savingsAccount", orphanRemoval = true) protected final List<SavingsAccountTransaction> transactions = new ArrayList<>(); @LazyCollection(LazyCollectionOption.FALSE) @OneToMany(cascade = CascadeType.ALL, mappedBy = "savingsAccount", orphanRemoval = true) protected Set<SavingsAccountCharge> charges = new HashSet<>(); @LazyCollection(LazyCollectionOption.FALSE) @OneToMany(cascade = CascadeType.ALL, mappedBy = "savingsAccount", orphanRemoval = true) private Set<SavingsOfficerAssignmentHistory> savingsOfficerHistory; @Transient protected boolean accountNumberRequiresAutoGeneration = false; @Transient protected SavingsAccountTransactionSummaryWrapper savingsAccountTransactionSummaryWrapper; @Transient protected SavingsHelper savingsHelper; @Column(name = "deposit_type_enum", insertable = false, updatable = false) private Integer depositType; @Column(name = "min_balance_for_interest_calculation", scale = 6, precision = 19, nullable = true) private BigDecimal minBalanceForInterestCalculation; protected SavingsAccount() { // } public static SavingsAccount createNewApplicationForSubmittal(final Client client, final Group group, final SavingsProduct product, final Staff fieldOfficer, final String accountNo, final String externalId, final AccountType accountType, final LocalDate submittedOnDate, final AppUser submittedBy, final BigDecimal interestRate, final SavingsCompoundingInterestPeriodType interestCompoundingPeriodType, final SavingsPostingInterestPeriodType interestPostingPeriodType, final SavingsInterestCalculationType interestCalculationType, final SavingsInterestCalculationDaysInYearType interestCalculationDaysInYearType, final BigDecimal minRequiredOpeningBalance, final Integer lockinPeriodFrequency, final SavingsPeriodFrequencyType lockinPeriodFrequencyType, final boolean withdrawalFeeApplicableForTransfer, final Set<SavingsAccountCharge> savingsAccountCharges, final boolean allowOverdraft, final BigDecimal overdraftLimit, final boolean enforceMinRequiredBalance, final BigDecimal minRequiredBalance, final BigDecimal nominalAnnualInterestRateOverdraft, final BigDecimal minOverdraftForInterestCalculation) { final SavingsAccountStatusType status = SavingsAccountStatusType.SUBMITTED_AND_PENDING_APPROVAL; return new SavingsAccount(client, group, product, fieldOfficer, accountNo, externalId, status, accountType, submittedOnDate, submittedBy, interestRate, interestCompoundingPeriodType, interestPostingPeriodType, interestCalculationType, interestCalculationDaysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType, withdrawalFeeApplicableForTransfer, savingsAccountCharges, allowOverdraft, overdraftLimit, enforceMinRequiredBalance, minRequiredBalance, nominalAnnualInterestRateOverdraft, minOverdraftForInterestCalculation); } protected SavingsAccount(final Client client, final Group group, final SavingsProduct product, final Staff fieldOfficer, final String accountNo, final String externalId, final SavingsAccountStatusType status, final AccountType accountType, final LocalDate submittedOnDate, final AppUser submittedBy, final BigDecimal nominalAnnualInterestRate, final SavingsCompoundingInterestPeriodType interestCompoundingPeriodType, final SavingsPostingInterestPeriodType interestPostingPeriodType, final SavingsInterestCalculationType interestCalculationType, final SavingsInterestCalculationDaysInYearType interestCalculationDaysInYearType, final BigDecimal minRequiredOpeningBalance, final Integer lockinPeriodFrequency, final SavingsPeriodFrequencyType lockinPeriodFrequencyType, final boolean withdrawalFeeApplicableForTransfer, final Set<SavingsAccountCharge> savingsAccountCharges, final boolean allowOverdraft, final BigDecimal overdraftLimit) { this(client, group, product, fieldOfficer, accountNo, externalId, status, accountType, submittedOnDate, submittedBy, nominalAnnualInterestRate, interestCompoundingPeriodType, interestPostingPeriodType, interestCalculationType, interestCalculationDaysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType, withdrawalFeeApplicableForTransfer, savingsAccountCharges, allowOverdraft, overdraftLimit, false, null, null, null); } protected SavingsAccount(final Client client, final Group group, final SavingsProduct product, final Staff savingsOfficer, final String accountNo, final String externalId, final SavingsAccountStatusType status, final AccountType accountType, final LocalDate submittedOnDate, final AppUser submittedBy, final BigDecimal nominalAnnualInterestRate, final SavingsCompoundingInterestPeriodType interestCompoundingPeriodType, final SavingsPostingInterestPeriodType interestPostingPeriodType, final SavingsInterestCalculationType interestCalculationType, final SavingsInterestCalculationDaysInYearType interestCalculationDaysInYearType, final BigDecimal minRequiredOpeningBalance, final Integer lockinPeriodFrequency, final SavingsPeriodFrequencyType lockinPeriodFrequencyType, final boolean withdrawalFeeApplicableForTransfer, final Set<SavingsAccountCharge> savingsAccountCharges, final boolean allowOverdraft, final BigDecimal overdraftLimit, final boolean enforceMinRequiredBalance, final BigDecimal minRequiredBalance, final BigDecimal nominalAnnualInterestRateOverdraft, final BigDecimal minOverdraftForInterestCalculation) { this.client = client; this.group = group; this.product = product; this.savingsOfficer = savingsOfficer; if (StringUtils.isBlank(accountNo)) { this.accountNumber = new RandomPasswordGenerator(19).generate(); this.accountNumberRequiresAutoGeneration = true; } else { this.accountNumber = accountNo; } this.currency = product.currency(); this.externalId = externalId; this.status = status.getValue(); this.accountType = accountType.getValue(); this.submittedOnDate = submittedOnDate.toDate(); this.submittedBy = submittedBy; this.nominalAnnualInterestRate = nominalAnnualInterestRate; this.interestCompoundingPeriodType = interestCompoundingPeriodType.getValue(); this.interestPostingPeriodType = interestPostingPeriodType.getValue(); this.interestCalculationType = interestCalculationType.getValue(); this.interestCalculationDaysInYearType = interestCalculationDaysInYearType.getValue(); this.minRequiredOpeningBalance = minRequiredOpeningBalance; this.lockinPeriodFrequency = lockinPeriodFrequency; if (lockinPeriodFrequencyType != null) { this.lockinPeriodFrequencyType = lockinPeriodFrequencyType.getValue(); } this.withdrawalFeeApplicableForTransfer = withdrawalFeeApplicableForTransfer; if (!CollectionUtils.isEmpty(savingsAccountCharges)) { this.charges = associateChargesWithThisSavingsAccount(savingsAccountCharges); } this.summary = new SavingsAccountSummary(); this.allowOverdraft = allowOverdraft; this.overdraftLimit = overdraftLimit; this.nominalAnnualInterestRateOverdraft = nominalAnnualInterestRateOverdraft; this.minOverdraftForInterestCalculation = minOverdraftForInterestCalculation; esnureOverdraftLimitsSetForOverdraftAccounts(); this.enforceMinRequiredBalance = enforceMinRequiredBalance; this.minRequiredBalance = minRequiredBalance; this.minBalanceForInterestCalculation = product.minBalanceForInterestCalculation(); this.savingsOfficerHistory = null; } /** * Used after fetching/hydrating a {@link SavingsAccount} object to inject * helper services/components used for update summary details after * events/transactions on a {@link SavingsAccount}. */ public void setHelpers(final SavingsAccountTransactionSummaryWrapper savingsAccountTransactionSummaryWrapper, final SavingsHelper savingsHelper) { this.savingsAccountTransactionSummaryWrapper = savingsAccountTransactionSummaryWrapper; this.savingsHelper = savingsHelper; } public boolean isNotActive() { return !isActive(); } public boolean isActive() { return SavingsAccountStatusType.fromInt(this.status).isActive(); } public boolean isNotSubmittedAndPendingApproval() { return !isSubmittedAndPendingApproval(); } public boolean isSubmittedAndPendingApproval() { return SavingsAccountStatusType.fromInt(this.status).isSubmittedAndPendingApproval(); } public boolean isApproved() { return SavingsAccountStatusType.fromInt(this.status).isApproved(); } public boolean isActivated() { boolean isActive = false; if (this.activatedOnDate != null) { isActive = true; } return isActive; } public boolean isClosed() { return SavingsAccountStatusType.fromInt(this.status).isClosed(); } public void postInterest(final MathContext mc, final LocalDate interestPostingUpToDate, final boolean isInterestTransfer, final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth) { final List<PostingPeriod> postingPeriods = calculateInterestUsing(mc, interestPostingUpToDate, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth); Money interestPostedToDate = Money.zero(this.currency); boolean recalucateDailyBalanceDetails = false; for (final PostingPeriod interestPostingPeriod : postingPeriods) { final LocalDate interestPostingTransactionDate = interestPostingPeriod.dateOfPostingTransaction(); final Money interestEarnedToBePostedForPeriod = interestPostingPeriod.getInterestEarned(); if (!interestPostingTransactionDate.isAfter(interestPostingUpToDate)) { interestPostedToDate = interestPostedToDate.plus(interestEarnedToBePostedForPeriod); final SavingsAccountTransaction postingTransaction = findInterestPostingTransactionFor( interestPostingTransactionDate); if (postingTransaction == null) { SavingsAccountTransaction newPostingTransaction; if (interestEarnedToBePostedForPeriod.isGreaterThanOrEqualTo(Money.zero(currency))) { newPostingTransaction = SavingsAccountTransaction.interestPosting(this, office(), interestPostingTransactionDate, interestEarnedToBePostedForPeriod); } else { newPostingTransaction = SavingsAccountTransaction.overdraftInterest(this, office(), interestPostingTransactionDate, interestEarnedToBePostedForPeriod.negated()); } this.transactions.add(newPostingTransaction); recalucateDailyBalanceDetails = true; } else { boolean correctionRequired = false; if (postingTransaction.isInterestPostingAndNotReversed()) { correctionRequired = postingTransaction.hasNotAmount(interestEarnedToBePostedForPeriod); } else { correctionRequired = postingTransaction .hasNotAmount(interestEarnedToBePostedForPeriod.negated()); } if (correctionRequired) { postingTransaction.reverse(); SavingsAccountTransaction newPostingTransaction; if (interestEarnedToBePostedForPeriod.isGreaterThanOrEqualTo(Money.zero(currency))) { newPostingTransaction = SavingsAccountTransaction.interestPosting(this, office(), interestPostingTransactionDate, interestEarnedToBePostedForPeriod); } else { newPostingTransaction = SavingsAccountTransaction.overdraftInterest(this, office(), interestPostingTransactionDate, interestEarnedToBePostedForPeriod.negated()); } this.transactions.add(newPostingTransaction); recalucateDailyBalanceDetails = true; } } } } if (recalucateDailyBalanceDetails) { // no openingBalance concept supported yet but probably will to // allow // for migrations. final Money openingAccountBalance = Money.zero(this.currency); // update existing transactions so derived balance fields are // correct. recalculateDailyBalances(openingAccountBalance, interestPostingUpToDate); } this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, this.transactions); } protected SavingsAccountTransaction findInterestPostingTransactionFor(final LocalDate postingDate) { SavingsAccountTransaction postingTransation = null; for (final SavingsAccountTransaction transaction : this.transactions) { if ((transaction.isInterestPostingAndNotReversed() || transaction.isOverdraftInterestAndNotReversed()) && transaction.occursOn(postingDate)) { postingTransation = transaction; break; } } return postingTransation; } // Determine the last transaction for given day protected SavingsAccountTransaction findLastTransaction(final LocalDate date) { SavingsAccountTransaction savingsTransaction = null; for (final SavingsAccountTransaction transaction : this.transactions) { if (transaction.isNotReversed() && transaction.occursOn(date)) { savingsTransaction = transaction; break; } } return savingsTransaction; } /** * All interest calculation based on END-OF-DAY-BALANCE. * * Interest calculation is performed on-the-fly over all account * transactions. * * * 1. Calculate Interest From Beginning Of Account 1a. determine the * 'crediting' periods that exist for this savings acccount 1b. determine * the 'compounding' periods that exist within each 'crediting' period * calculate the amount of interest due at the end of each 'crediting' * period check if an existing 'interest posting' transaction exists for * date and matches the amount posted * * @param isInterestTransfer * TODO */ public List<PostingPeriod> calculateInterestUsing(final MathContext mc, final LocalDate upToInterestCalculationDate, boolean isInterestTransfer, final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth) { // no openingBalance concept supported yet but probably will to allow // for migrations. final Money openingAccountBalance = Money.zero(this.currency); // update existing transactions so derived balance fields are // correct. recalculateDailyBalances(openingAccountBalance, upToInterestCalculationDate); // 1. default to calculate interest based on entire history OR // 2. determine latest 'posting period' and find interest credited to // that period // A generate list of EndOfDayBalances (not including interest postings) final SavingsPostingInterestPeriodType postingPeriodType = SavingsPostingInterestPeriodType .fromInt(this.interestPostingPeriodType); final SavingsCompoundingInterestPeriodType compoundingPeriodType = SavingsCompoundingInterestPeriodType .fromInt(this.interestCompoundingPeriodType); final SavingsInterestCalculationDaysInYearType daysInYearType = SavingsInterestCalculationDaysInYearType .fromInt(this.interestCalculationDaysInYearType); final List<LocalDateInterval> postingPeriodIntervals = this.savingsHelper.determineInterestPostingPeriods( getStartInterestCalculationDate(), upToInterestCalculationDate, postingPeriodType, financialYearBeginningMonth); final List<PostingPeriod> allPostingPeriods = new ArrayList<>(); Money periodStartingBalance; if (this.startInterestCalculationDate != null) { LocalDate startInterestCalculationDate = new LocalDate(this.startInterestCalculationDate); final SavingsAccountTransaction transaction = findLastTransaction(startInterestCalculationDate); if (transaction == null) { final String defaultUserMessage = "No transactions were found on the specified date " + getStartInterestCalculationDate().toString() + " for account number " + this.accountNumber.toString() + " and resource id " + getId(); final ApiParameterError error = ApiParameterError.parameterError( "error.msg.savingsaccount.transaction.incorrect.start.interest.calculation.date", defaultUserMessage, "transactionDate", getStartInterestCalculationDate().toString()); final List<ApiParameterError> dataValidationErrors = new ArrayList<>(); dataValidationErrors.add(error); throw new PlatformApiDataValidationException(dataValidationErrors); } periodStartingBalance = transaction.getRunningBalance(this.currency); } else periodStartingBalance = Money.zero(this.currency); final SavingsInterestCalculationType interestCalculationType = SavingsInterestCalculationType .fromInt(this.interestCalculationType); final BigDecimal interestRateAsFraction = getEffectiveInterestRateAsFraction(mc, upToInterestCalculationDate); final BigDecimal overdraftInterestRateAsFraction = getEffectiveOverdraftInterestRateAsFraction(mc); final Collection<Long> interestPostTransactions = this.savingsHelper .fetchPostInterestTransactionIds(getId()); final Money minBalanceForInterestCalculation = Money.of(getCurrency(), minBalanceForInterestCalculation()); final Money minOverdraftForInterestCalculation = Money.of(getCurrency(), this.minOverdraftForInterestCalculation); for (final LocalDateInterval periodInterval : postingPeriodIntervals) { final PostingPeriod postingPeriod = PostingPeriod.createFrom(periodInterval, periodStartingBalance, retreiveOrderedNonInterestPostingTransactions(), this.currency, compoundingPeriodType, interestCalculationType, interestRateAsFraction, daysInYearType.getValue(), upToInterestCalculationDate, interestPostTransactions, isInterestTransfer, minBalanceForInterestCalculation, isSavingsInterestPostingAtCurrentPeriodEnd, overdraftInterestRateAsFraction, minOverdraftForInterestCalculation); periodStartingBalance = postingPeriod.closingBalance(); allPostingPeriods.add(postingPeriod); } this.savingsHelper.calculateInterestForAllPostingPeriods(this.currency, allPostingPeriods, getLockedInUntilLocalDate(), isTransferInterestToOtherAccount()); this.summary.updateFromInterestPeriodSummaries(this.currency, allPostingPeriods); this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, this.transactions); return allPostingPeriods; } private BigDecimal getEffectiveOverdraftInterestRateAsFraction(MathContext mc) { return this.nominalAnnualInterestRateOverdraft.divide(BigDecimal.valueOf(100l), mc); } @SuppressWarnings("unused") protected BigDecimal getEffectiveInterestRateAsFraction(final MathContext mc, final LocalDate upToInterestCalculationDate) { return this.nominalAnnualInterestRate.divide(BigDecimal.valueOf(100l), mc); } protected List<SavingsAccountTransaction> retreiveOrderedNonInterestPostingTransactions() { final List<SavingsAccountTransaction> listOfTransactionsSorted = retreiveListOfTransactions(); final List<SavingsAccountTransaction> orderedNonInterestPostingTransactions = new ArrayList<>(); for (final SavingsAccountTransaction transaction : listOfTransactionsSorted) { if (!(transaction.isInterestPostingAndNotReversed() || transaction.isOverdraftInterestAndNotReversed()) && transaction.isNotReversed()) { orderedNonInterestPostingTransactions.add(transaction); } } return orderedNonInterestPostingTransactions; } protected List<SavingsAccountTransaction> retreiveListOfTransactions() { final List<SavingsAccountTransaction> listOfTransactionsSorted = new ArrayList<>(); listOfTransactionsSorted.addAll(this.transactions); final SavingsAccountTransactionComparator transactionComparator = new SavingsAccountTransactionComparator(); Collections.sort(listOfTransactionsSorted, transactionComparator); return listOfTransactionsSorted; } protected void recalculateDailyBalances(final Money openingAccountBalance, final LocalDate interestPostingUpToDate) { Money runningBalance = openingAccountBalance.copy(); List<SavingsAccountTransaction> accountTransactionsSorted = retreiveListOfTransactions(); boolean isTransactionsModified = false; for (final SavingsAccountTransaction transaction : accountTransactionsSorted) { if (transaction.isReversed()) { transaction.zeroBalanceFields(); } else { Money overdraftAmount = Money.zero(this.currency); Money transactionAmount = Money.zero(this.currency); if (transaction.isCredit()) { if (runningBalance.isLessThanZero()) { Money diffAmount = transaction.getAmount(this.currency).plus(runningBalance); if (diffAmount.isGreaterThanZero()) { overdraftAmount = transaction.getAmount(this.currency).minus(diffAmount); } else { overdraftAmount = transaction.getAmount(this.currency); } } transactionAmount = transactionAmount.plus(transaction.getAmount(this.currency)); } else if (transaction.isDebit()) { if (runningBalance.isLessThanZero()) { overdraftAmount = transaction.getAmount(this.currency); } transactionAmount = transactionAmount.minus(transaction.getAmount(this.currency)); } runningBalance = runningBalance.plus(transactionAmount); transaction.updateRunningBalance(runningBalance); if (overdraftAmount.isZero() && runningBalance.isLessThanZero()) { overdraftAmount = overdraftAmount.plus(runningBalance.getAmount().negate()); } if (transaction.getId() == null && overdraftAmount.isGreaterThanZero()) { transaction.updateOverdraftAmount(overdraftAmount.getAmount()); } else if (overdraftAmount.isNotEqualTo(transaction.getOverdraftAmount(getCurrency()))) { SavingsAccountTransaction accountTransaction = SavingsAccountTransaction .copyTransaction(transaction); transaction.reverse(); if (overdraftAmount.isGreaterThanZero()) { accountTransaction.updateOverdraftAmount(overdraftAmount.getAmount()); } accountTransaction.updateRunningBalance(runningBalance); this.transactions.add(accountTransaction); isTransactionsModified = true; } } } if (isTransactionsModified) { accountTransactionsSorted = retreiveListOfTransactions(); } resetAccountTransactionsEndOfDayBalances(accountTransactionsSorted, interestPostingUpToDate); } protected void resetAccountTransactionsEndOfDayBalances( final List<SavingsAccountTransaction> accountTransactionsSorted, final LocalDate interestPostingUpToDate) { // loop over transactions in reverse LocalDate endOfBalanceDate = interestPostingUpToDate; for (int i = accountTransactionsSorted.size() - 1; i >= 0; i--) { final SavingsAccountTransaction transaction = accountTransactionsSorted.get(i); if (transaction.isNotReversed() && !(transaction.isInterestPostingAndNotReversed() || transaction.isOverdraftInterestAndNotReversed())) { transaction.updateCumulativeBalanceAndDates(this.currency, endOfBalanceDate); // this transactions transaction date is end of balance date for // previous transaction. endOfBalanceDate = transaction.transactionLocalDate().minusDays(1); } } } public SavingsAccountTransaction deposit(final SavingsAccountTransactionDTO transactionDTO) { final String resourceTypeName = depositAccountType().resourceName(); if (isNotActive()) { final String defaultUserMessage = "Transaction is not allowed. Account is not active."; final ApiParameterError error = ApiParameterError.parameterError( "error.msg." + resourceTypeName + ".transaction.account.is.not.active", defaultUserMessage, "transactionDate", transactionDTO.getTransactionDate().toString(transactionDTO.getFormatter())); final List<ApiParameterError> dataValidationErrors = new ArrayList<>(); dataValidationErrors.add(error); throw new PlatformApiDataValidationException(dataValidationErrors); } if (isDateInTheFuture(transactionDTO.getTransactionDate())) { final String defaultUserMessage = "Transaction date cannot be in the future."; final ApiParameterError error = ApiParameterError.parameterError( "error.msg." + resourceTypeName + ".transaction.in.the.future", defaultUserMessage, "transactionDate", transactionDTO.getTransactionDate().toString(transactionDTO.getFormatter())); final List<ApiParameterError> dataValidationErrors = new ArrayList<>(); dataValidationErrors.add(error); throw new PlatformApiDataValidationException(dataValidationErrors); } if (transactionDTO.getTransactionDate().isBefore(getActivationLocalDate())) { final Object[] defaultUserArgs = Arrays .asList(transactionDTO.getTransactionDate().toString(transactionDTO.getFormatter()), getActivationLocalDate().toString(transactionDTO.getFormatter())) .toArray(); final String defaultUserMessage = "Transaction date cannot be before accounts activation date."; final ApiParameterError error = ApiParameterError.parameterError( "error.msg." + resourceTypeName + ".transaction.before.activation.date", defaultUserMessage, "transactionDate", defaultUserArgs); final List<ApiParameterError> dataValidationErrors = new ArrayList<>(); dataValidationErrors.add(error); throw new PlatformApiDataValidationException(dataValidationErrors); } validateActivityNotBeforeClientOrGroupTransferDate(SavingsEvent.SAVINGS_DEPOSIT, transactionDTO.getTransactionDate()); final Money amount = Money.of(this.currency, transactionDTO.getTransactionAmount()); final SavingsAccountTransaction transaction = SavingsAccountTransaction.deposit(this, office(), transactionDTO.getPaymentDetail(), transactionDTO.getTransactionDate(), amount, transactionDTO.getCreatedDate(), transactionDTO.getAppUser()); this.transactions.add(transaction); this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, this.transactions); return transaction; } public LocalDate getActivationLocalDate() { LocalDate activationLocalDate = null; if (this.activatedOnDate != null) { activationLocalDate = new LocalDate(this.activatedOnDate); } return activationLocalDate; } // startInterestCalculationDate is set during migration so that there is no // interference with interest posting of previous system public LocalDate getStartInterestCalculationDate() { LocalDate startInterestCalculationLocalDate = null; if (this.startInterestCalculationDate != null) { startInterestCalculationLocalDate = new LocalDate(this.startInterestCalculationDate); } else startInterestCalculationLocalDate = getActivationLocalDate(); return startInterestCalculationLocalDate; } public SavingsAccountTransaction withdraw(final SavingsAccountTransactionDTO transactionDTO, final boolean applyWithdrawFee) { if (!isTransactionsAllowed()) { final String defaultUserMessage = "Transaction is not allowed. Account is not active."; final ApiParameterError error = ApiParameterError.parameterError( "error.msg.savingsaccount.transaction.account.is.not.active", defaultUserMessage, "transactionDate", transactionDTO.getTransactionDate().toString(transactionDTO.getFormatter())); final List<ApiParameterError> dataValidationErrors = new ArrayList<>(); dataValidationErrors.add(error); throw new PlatformApiDataValidationException(dataValidationErrors); } if (isDateInTheFuture(transactionDTO.getTransactionDate())) { final String defaultUserMessage = "Transaction date cannot be in the future."; final ApiParameterError error = ApiParameterError.parameterError( "error.msg.savingsaccount.transaction.in.the.future", defaultUserMessage, "transactionDate", transactionDTO.getTransactionDate().toString(transactionDTO.getFormatter())); final List<ApiParameterError> dataValidationErrors = new ArrayList<>(); dataValidationErrors.add(error); throw new PlatformApiDataValidationException(dataValidationErrors); } if (transactionDTO.getTransactionDate().isBefore(getActivationLocalDate())) { final Object[] defaultUserArgs = Arrays .asList(transactionDTO.getTransactionDate().toString(transactionDTO.getFormatter()), getActivationLocalDate().toString(transactionDTO.getFormatter())) .toArray(); final String defaultUserMessage = "Transaction date cannot be before accounts activation date."; final ApiParameterError error = ApiParameterError.parameterError( "error.msg.savingsaccount.transaction.before.activation.date", defaultUserMessage, "transactionDate", defaultUserArgs); final List<ApiParameterError> dataValidationErrors = new ArrayList<>(); dataValidationErrors.add(error); throw new PlatformApiDataValidationException(dataValidationErrors); } if (isAccountLocked(transactionDTO.getTransactionDate())) { final String defaultUserMessage = "Withdrawal is not allowed. No withdrawals are allowed until after " + getLockedInUntilLocalDate().toString(transactionDTO.getFormatter()); final ApiParameterError error = ApiParameterError.parameterError( "error.msg.savingsaccount.transaction.withdrawals.blocked.during.lockin.period", defaultUserMessage, "transactionDate", transactionDTO.getTransactionDate().toString(transactionDTO.getFormatter()), getLockedInUntilLocalDate().toString(transactionDTO.getFormatter())); final List<ApiParameterError> dataValidationErrors = new ArrayList<>(); dataValidationErrors.add(error); throw new PlatformApiDataValidationException(dataValidationErrors); } validateActivityNotBeforeClientOrGroupTransferDate(SavingsEvent.SAVINGS_WITHDRAWAL, transactionDTO.getTransactionDate()); final Money transactionAmountMoney = Money.of(this.currency, transactionDTO.getTransactionAmount()); final SavingsAccountTransaction transaction = SavingsAccountTransaction.withdrawal(this, office(), transactionDTO.getPaymentDetail(), transactionDTO.getTransactionDate(), transactionAmountMoney, transactionDTO.getCreatedDate(), transactionDTO.getAppUser()); this.transactions.add(transaction); if (applyWithdrawFee) { // auto pay withdrawal fee payWithdrawalFee(transactionDTO.getTransactionAmount(), transactionDTO.getTransactionDate(), transactionDTO.getAppUser()); } return transaction; } private void payWithdrawalFee(final BigDecimal transactionAmoount, final LocalDate transactionDate, final AppUser user) { for (SavingsAccountCharge charge : this.charges()) { if (charge.isWithdrawalFee() && charge.isActive()) { charge.updateWithdralFeeAmount(transactionAmoount); this.payCharge(charge, charge.getAmountOutstanding(this.getCurrency()), transactionDate, user); } } } public boolean isBeforeLastPostingPeriod(final LocalDate transactionDate) { boolean transactionBeforeLastInterestPosting = false; for (final SavingsAccountTransaction transaction : retreiveListOfTransactions()) { if ((transaction.isInterestPostingAndNotReversed() || transaction.isOverdraftInterestAndNotReversed()) && transaction.isAfter(transactionDate)) { transactionBeforeLastInterestPosting = true; break; } } return transactionBeforeLastInterestPosting; } public void validateAccountBalanceDoesNotBecomeNegative(final BigDecimal transactionAmount, final boolean isException, final List<DepositAccountOnHoldTransaction> depositAccountOnHoldTransactions) { final List<SavingsAccountTransaction> transactionsSortedByDate = retreiveListOfTransactions(); Money runningBalance = Money.zero(this.currency); Money minRequiredBalance = minRequiredBalanceDerived(getCurrency()); LocalDate lastSavingsDate = null; for (final SavingsAccountTransaction transaction : transactionsSortedByDate) { if (transaction.isNotReversed() && transaction.isCredit()) { runningBalance = runningBalance.plus(transaction.getAmount(this.currency)); } else if (transaction.isNotReversed() && transaction.isDebit()) { runningBalance = runningBalance.minus(transaction.getAmount(this.currency)); } else { continue; } final BigDecimal withdrawalFee = null; /* * Loop through the onHold funds and see if we need to deduct or add * to minimum required balance and the point in time the transaction * was made: */ if (depositAccountOnHoldTransactions != null) { for (final DepositAccountOnHoldTransaction onHoldTransaction : depositAccountOnHoldTransactions) { // Compare the balance of the on hold: if ((onHoldTransaction.getTransactionDate().isBefore(transaction.transactionLocalDate()) || onHoldTransaction.getTransactionDate().isEqual(transaction.transactionLocalDate())) && (lastSavingsDate == null || onHoldTransaction.getTransactionDate().isAfter(lastSavingsDate))) { if (onHoldTransaction.getTransactionType().isHold()) { minRequiredBalance = minRequiredBalance .plus(onHoldTransaction.getAmountMoney(this.currency)); } else { minRequiredBalance = minRequiredBalance .minus(onHoldTransaction.getAmountMoney(this.currency)); } } } } // deal with potential minRequiredBalance and // enforceMinRequiredBalance if (!isException && transaction.canProcessBalanceCheck()) { if (runningBalance.minus(minRequiredBalance).isLessThanZero()) { throw new InsufficientAccountBalanceException("transactionAmount", getAccountBalance(), withdrawalFee, transactionAmount); } } lastSavingsDate = transaction.transactionLocalDate(); } } public void validateAccountBalanceDoesNotBecomeNegative(final String transactionAction, final List<DepositAccountOnHoldTransaction> depositAccountOnHoldTransactions) { final List<SavingsAccountTransaction> transactionsSortedByDate = retreiveListOfTransactions(); Money runningBalance = Money.zero(this.currency); Money minRequiredBalance = minRequiredBalanceDerived(getCurrency()); LocalDate lastSavingsDate = null; for (final SavingsAccountTransaction transaction : transactionsSortedByDate) { if (transaction.isNotReversed() && transaction.isCredit()) { runningBalance = runningBalance.plus(transaction.getAmount(this.currency)); } else if (transaction.isNotReversed() && transaction.isDebit()) { runningBalance = runningBalance.minus(transaction.getAmount(this.currency)); } /* * Loop through the onHold funds and see if we need to deduct or add * to minimum required balance and the point in time the transaction * was made: */ if (depositAccountOnHoldTransactions != null) { for (final DepositAccountOnHoldTransaction onHoldTransaction : depositAccountOnHoldTransactions) { // Compare the balance of the on hold: if ((onHoldTransaction.getTransactionDate().isBefore(transaction.transactionLocalDate()) || onHoldTransaction.getTransactionDate().isEqual(transaction.transactionLocalDate())) && (lastSavingsDate == null || onHoldTransaction.getTransactionDate().isAfter(lastSavingsDate))) { if (onHoldTransaction.getTransactionType().isHold()) { minRequiredBalance = minRequiredBalance .plus(onHoldTransaction.getAmountMoney(this.currency)); } else { minRequiredBalance = minRequiredBalance .minus(onHoldTransaction.getAmountMoney(this.currency)); } } } } // enforceMinRequiredBalance if (transaction.canProcessBalanceCheck()) { if (runningBalance.minus(minRequiredBalance).isLessThanZero()) { final List<ApiParameterError> dataValidationErrors = new ArrayList<>(); final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) .resource(depositAccountType().resourceName() + transactionAction); if (this.allowOverdraft) { baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode( "results.in.balance.exceeding.overdraft.limit"); } else { baseDataValidator.reset() .failWithCodeNoParameterAddedToErrorCode("results.in.balance.going.negative"); } if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } } lastSavingsDate = transaction.transactionLocalDate(); } } protected boolean isAccountLocked(final LocalDate transactionDate) { boolean isLocked = false; final boolean accountHasLockedInSetting = this.lockedInUntilDate != null; if (accountHasLockedInSetting) { isLocked = getLockedInUntilLocalDate().isAfter(transactionDate); } return isLocked; } protected LocalDate getLockedInUntilLocalDate() { LocalDate lockedInUntilLocalDate = null; if (this.lockedInUntilDate != null) { lockedInUntilLocalDate = new LocalDate(this.lockedInUntilDate); } return lockedInUntilLocalDate; } private boolean isDateInTheFuture(final LocalDate transactionDate) { return transactionDate.isAfter(DateUtils.getLocalDateOfTenant()); } protected BigDecimal getAccountBalance() { return this.summary.getAccountBalance(this.currency).getAmount(); } public void modifyApplication(final JsonCommand command, final Map<String, Object> actualChanges) { final List<ApiParameterError> dataValidationErrors = new ArrayList<>(); final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) .resource(SAVINGS_ACCOUNT_RESOURCE_NAME + SavingsApiConstants.modifyApplicationAction); this.modifyApplication(command, actualChanges, baseDataValidator); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } public void modifyApplication(final JsonCommand command, final Map<String, Object> actualChanges, final DataValidatorBuilder baseDataValidator) { final SavingsAccountStatusType currentStatus = SavingsAccountStatusType.fromInt(this.status); if (!SavingsAccountStatusType.SUBMITTED_AND_PENDING_APPROVAL.hasStateOf(currentStatus)) { baseDataValidator.reset() .failWithCodeNoParameterAddedToErrorCode("not.in.submittedandpendingapproval.state"); return; } final String localeAsInput = command.locale(); final String dateFormat = command.dateFormat(); if (command.isChangeInLocalDateParameterNamed(SavingsApiConstants.submittedOnDateParamName, getSubmittedOnLocalDate())) { final LocalDate newValue = command .localDateValueOfParameterNamed(SavingsApiConstants.submittedOnDateParamName); final String newValueAsString = command .stringValueOfParameterNamed(SavingsApiConstants.submittedOnDateParamName); actualChanges.put(SavingsApiConstants.submittedOnDateParamName, newValueAsString); actualChanges.put(SavingsApiConstants.localeParamName, localeAsInput); actualChanges.put(SavingsApiConstants.dateFormatParamName, dateFormat); this.submittedOnDate = newValue.toDate(); } if (command.isChangeInStringParameterNamed(SavingsApiConstants.accountNoParamName, this.accountNumber)) { final String newValue = command.stringValueOfParameterNamed(SavingsApiConstants.accountNoParamName); actualChanges.put(SavingsApiConstants.accountNoParamName, newValue); this.accountNumber = StringUtils.defaultIfEmpty(newValue, null); } if (command.isChangeInStringParameterNamed(SavingsApiConstants.externalIdParamName, this.externalId)) { final String newValue = command.stringValueOfParameterNamed(SavingsApiConstants.externalIdParamName); actualChanges.put(SavingsApiConstants.externalIdParamName, newValue); this.externalId = StringUtils.defaultIfEmpty(newValue, null); } if (command.isChangeInLongParameterNamed(SavingsApiConstants.clientIdParamName, clientId())) { final Long newValue = command.longValueOfParameterNamed(SavingsApiConstants.clientIdParamName); actualChanges.put(SavingsApiConstants.clientIdParamName, newValue); } if (command.isChangeInLongParameterNamed(SavingsApiConstants.groupIdParamName, groupId())) { final Long newValue = command.longValueOfParameterNamed(SavingsApiConstants.groupIdParamName); actualChanges.put(SavingsApiConstants.groupIdParamName, newValue); } if (command.isChangeInLongParameterNamed(SavingsApiConstants.productIdParamName, this.product.getId())) { final Long newValue = command.longValueOfParameterNamed(SavingsApiConstants.productIdParamName); actualChanges.put(SavingsApiConstants.productIdParamName, newValue); } if (command.isChangeInLongParameterNamed(SavingsApiConstants.fieldOfficerIdParamName, hasSavingsOfficerId())) { final Long newValue = command.longValueOfParameterNamed(SavingsApiConstants.fieldOfficerIdParamName); actualChanges.put(SavingsApiConstants.fieldOfficerIdParamName, newValue); } if (command.isChangeInBigDecimalParameterNamed(SavingsApiConstants.nominalAnnualInterestRateParamName, this.nominalAnnualInterestRate)) { final BigDecimal newValue = command .bigDecimalValueOfParameterNamed(SavingsApiConstants.nominalAnnualInterestRateParamName); actualChanges.put(SavingsApiConstants.nominalAnnualInterestRateParamName, newValue); actualChanges.put("locale", localeAsInput); this.nominalAnnualInterestRate = newValue; } if (command.isChangeInIntegerParameterNamed(SavingsApiConstants.interestCompoundingPeriodTypeParamName, this.interestCompoundingPeriodType)) { final Integer newValue = command .integerValueOfParameterNamed(SavingsApiConstants.interestCompoundingPeriodTypeParamName); this.interestCompoundingPeriodType = newValue != null ? SavingsCompoundingInterestPeriodType.fromInt(newValue).getValue() : newValue; actualChanges.put(SavingsApiConstants.interestCompoundingPeriodTypeParamName, this.interestCompoundingPeriodType); } if (command.isChangeInIntegerParameterNamed(SavingsApiConstants.interestPostingPeriodTypeParamName, this.interestPostingPeriodType)) { final Integer newValue = command .integerValueOfParameterNamed(SavingsApiConstants.interestPostingPeriodTypeParamName); this.interestPostingPeriodType = newValue != null ? SavingsPostingInterestPeriodType.fromInt(newValue).getValue() : newValue; actualChanges.put(SavingsApiConstants.interestPostingPeriodTypeParamName, this.interestPostingPeriodType); } if (command.isChangeInIntegerParameterNamed(SavingsApiConstants.interestCalculationTypeParamName, this.interestCalculationType)) { final Integer newValue = command .integerValueOfParameterNamed(SavingsApiConstants.interestCalculationTypeParamName); this.interestCalculationType = newValue != null ? SavingsInterestCalculationType.fromInt(newValue).getValue() : newValue; actualChanges.put(SavingsApiConstants.interestCalculationTypeParamName, this.interestCalculationType); } if (command.isChangeInIntegerParameterNamed(SavingsApiConstants.interestCalculationDaysInYearTypeParamName, this.interestCalculationDaysInYearType)) { final Integer newValue = command .integerValueOfParameterNamed(SavingsApiConstants.interestCalculationDaysInYearTypeParamName); this.interestCalculationDaysInYearType = newValue != null ? SavingsInterestCalculationDaysInYearType.fromInt(newValue).getValue() : newValue; actualChanges.put(SavingsApiConstants.interestCalculationDaysInYearTypeParamName, this.interestCalculationDaysInYearType); } if (command.isChangeInBigDecimalParameterNamedDefaultingZeroToNull( SavingsApiConstants.minRequiredOpeningBalanceParamName, this.minRequiredOpeningBalance)) { final BigDecimal newValue = command.bigDecimalValueOfParameterNamedDefaultToNullIfZero( SavingsApiConstants.minRequiredOpeningBalanceParamName); actualChanges.put(SavingsApiConstants.minRequiredOpeningBalanceParamName, newValue); actualChanges.put("locale", localeAsInput); this.minRequiredOpeningBalance = Money.of(this.currency, newValue).getAmount(); } if (command.isChangeInIntegerParameterNamedDefaultingZeroToNull( SavingsApiConstants.lockinPeriodFrequencyParamName, this.lockinPeriodFrequency)) { final Integer newValue = command.integerValueOfParameterNamedDefaultToNullIfZero( SavingsApiConstants.lockinPeriodFrequencyParamName); actualChanges.put(SavingsApiConstants.lockinPeriodFrequencyParamName, newValue); actualChanges.put("locale", localeAsInput); this.lockinPeriodFrequency = newValue; } if (command.isChangeInIntegerParameterNamed(SavingsApiConstants.lockinPeriodFrequencyTypeParamName, this.lockinPeriodFrequencyType)) { final Integer newValue = command .integerValueOfParameterNamed(SavingsApiConstants.lockinPeriodFrequencyTypeParamName); actualChanges.put(SavingsApiConstants.lockinPeriodFrequencyTypeParamName, newValue); this.lockinPeriodFrequencyType = newValue != null ? SavingsPeriodFrequencyType.fromInt(newValue).getValue() : newValue; } // set period type to null if frequency is null if (this.lockinPeriodFrequency == null) { this.lockinPeriodFrequencyType = null; } if (command.isChangeInBooleanParameterNamed(withdrawalFeeForTransfersParamName, this.withdrawalFeeApplicableForTransfer)) { final boolean newValue = command .booleanPrimitiveValueOfParameterNamed(withdrawalFeeForTransfersParamName); actualChanges.put(withdrawalFeeForTransfersParamName, newValue); this.withdrawalFeeApplicableForTransfer = newValue; } // charges final String chargesParamName = "charges"; if (command.hasParameter(chargesParamName)) { final JsonArray jsonArray = command.arrayOfParameterNamed(chargesParamName); if (jsonArray != null) { actualChanges.put(chargesParamName, command.jsonFragment(chargesParamName)); } } if (command.isChangeInBooleanParameterNamed(allowOverdraftParamName, this.allowOverdraft)) { final boolean newValue = command.booleanPrimitiveValueOfParameterNamed(allowOverdraftParamName); actualChanges.put(allowOverdraftParamName, newValue); this.allowOverdraft = newValue; } if (command.isChangeInBigDecimalParameterNamedDefaultingZeroToNull(overdraftLimitParamName, this.overdraftLimit)) { final BigDecimal newValue = command .bigDecimalValueOfParameterNamedDefaultToNullIfZero(overdraftLimitParamName); actualChanges.put(overdraftLimitParamName, newValue); actualChanges.put(localeParamName, localeAsInput); this.overdraftLimit = newValue; } if (command.isChangeInBigDecimalParameterNamedDefaultingZeroToNull( nominalAnnualInterestRateOverdraftParamName, this.nominalAnnualInterestRateOverdraft)) { final BigDecimal newValue = command.bigDecimalValueOfParameterNamedDefaultToNullIfZero( nominalAnnualInterestRateOverdraftParamName); actualChanges.put(nominalAnnualInterestRateOverdraftParamName, newValue); actualChanges.put(localeParamName, localeAsInput); this.nominalAnnualInterestRateOverdraft = newValue; } if (command.isChangeInBigDecimalParameterNamedDefaultingZeroToNull( minOverdraftForInterestCalculationParamName, this.minOverdraftForInterestCalculation)) { final BigDecimal newValue = command.bigDecimalValueOfParameterNamedDefaultToNullIfZero( minOverdraftForInterestCalculationParamName); actualChanges.put(minOverdraftForInterestCalculationParamName, newValue); actualChanges.put(localeParamName, localeAsInput); this.minOverdraftForInterestCalculation = newValue; } if (!this.allowOverdraft) { this.overdraftLimit = null; this.nominalAnnualInterestRateOverdraft = null; this.minOverdraftForInterestCalculation = null; } if (command.isChangeInBooleanParameterNamed(enforceMinRequiredBalanceParamName, this.enforceMinRequiredBalance)) { final boolean newValue = command .booleanPrimitiveValueOfParameterNamed(enforceMinRequiredBalanceParamName); actualChanges.put(enforceMinRequiredBalanceParamName, newValue); this.enforceMinRequiredBalance = newValue; } if (command.isChangeInBigDecimalParameterNamedDefaultingZeroToNull(minRequiredBalanceParamName, this.minRequiredBalance)) { final BigDecimal newValue = command .bigDecimalValueOfParameterNamedDefaultToNullIfZero(minRequiredBalanceParamName); actualChanges.put(minRequiredBalanceParamName, newValue); actualChanges.put(localeParamName, localeAsInput); this.minRequiredBalance = newValue; } validateLockinDetails(baseDataValidator); esnureOverdraftLimitsSetForOverdraftAccounts(); } /** * If overdrafts are allowed and the overdraft limit is not set, set the * same to Zero **/ private void esnureOverdraftLimitsSetForOverdraftAccounts() { this.overdraftLimit = this.overdraftLimit == null ? BigDecimal.ZERO : this.overdraftLimit; this.nominalAnnualInterestRateOverdraft = this.nominalAnnualInterestRateOverdraft == null ? BigDecimal.ZERO : this.nominalAnnualInterestRateOverdraft; this.minOverdraftForInterestCalculation = this.minOverdraftForInterestCalculation == null ? BigDecimal.ZERO : this.minOverdraftForInterestCalculation; } private void validateLockinDetails(final DataValidatorBuilder baseDataValidator) { /* * final List<ApiParameterError> dataValidationErrors = new * ArrayList<ApiParameterError>(); final DataValidatorBuilder * baseDataValidator = new DataValidatorBuilder(dataValidationErrors) * .resource(resourceName); */ if (this.lockinPeriodFrequency == null) { baseDataValidator.reset().parameter(lockinPeriodFrequencyTypeParamName) .value(this.lockinPeriodFrequencyType).ignoreIfNull().inMinMaxRange(0, 3); if (this.lockinPeriodFrequencyType != null) { baseDataValidator.reset().parameter(lockinPeriodFrequencyParamName) .value(this.lockinPeriodFrequency).notNull().integerZeroOrGreater(); } } else { baseDataValidator.reset().parameter(lockinPeriodFrequencyParamName) .value(this.lockinPeriodFrequencyType).integerZeroOrGreater(); baseDataValidator.reset().parameter(lockinPeriodFrequencyTypeParamName) .value(this.lockinPeriodFrequencyType).notNull().inMinMaxRange(0, 3); } } public Map<String, Object> deriveAccountingBridgeData(final CurrencyData currencyData, final Set<Long> existingTransactionIds, final Set<Long> existingReversedTransactionIds, boolean isAccountTransfer) { final Map<String, Object> accountingBridgeData = new LinkedHashMap<>(); accountingBridgeData.put("savingsId", getId()); accountingBridgeData.put("savingsProductId", productId()); accountingBridgeData.put("currency", currencyData); accountingBridgeData.put("officeId", officeId()); accountingBridgeData.put("cashBasedAccountingEnabled", isCashBasedAccountingEnabledOnSavingsProduct()); accountingBridgeData.put("accrualBasedAccountingEnabled", isAccrualBasedAccountingEnabledOnSavingsProduct()); accountingBridgeData.put("isAccountTransfer", isAccountTransfer); final List<Map<String, Object>> newSavingsTransactions = new ArrayList<>(); for (final SavingsAccountTransaction transaction : this.transactions) { if (transaction.isReversed() && !existingReversedTransactionIds.contains(transaction.getId())) { newSavingsTransactions.add(transaction.toMapData(currencyData)); } else if (!existingTransactionIds.contains(transaction.getId())) { newSavingsTransactions.add(transaction.toMapData(currencyData)); } } accountingBridgeData.put("newSavingsTransactions", newSavingsTransactions); return accountingBridgeData; } public Collection<Long> findExistingTransactionIds() { final Collection<Long> ids = new ArrayList<>(); for (final SavingsAccountTransaction transaction : this.transactions) { ids.add(transaction.getId()); } return ids; } public Collection<Long> findExistingReversedTransactionIds() { final Collection<Long> ids = new ArrayList<>(); for (final SavingsAccountTransaction transaction : this.transactions) { if (transaction.isReversed()) { ids.add(transaction.getId()); } } return ids; } public void update(final Client client) { this.client = client; } public void update(final Group group) { this.group = group; } public void update(final SavingsProduct product) { this.product = product; this.minBalanceForInterestCalculation = product.minBalanceForInterestCalculation(); } public void update(final Staff savingsOfficer) { this.savingsOfficer = savingsOfficer; } public void updateAccountNo(final String newAccountNo) { this.accountNumber = newAccountNo; this.accountNumberRequiresAutoGeneration = false; } public boolean isAccountNumberRequiresAutoGeneration() { return this.accountNumberRequiresAutoGeneration; } public Long productId() { return this.product.getId(); } public SavingsProduct savingsProduct() { return this.product; } private Boolean isCashBasedAccountingEnabledOnSavingsProduct() { return this.product.isCashBasedAccountingEnabled(); } private Boolean isAccrualBasedAccountingEnabledOnSavingsProduct() { return this.product.isAccrualBasedAccountingEnabled(); } public Long officeId() { Long officeId = null; if (this.client != null) { officeId = this.client.officeId(); } else { officeId = this.group.officeId(); } return officeId; } public Office office() { Office office = null; if (this.client != null) { office = this.client.getOffice(); } else { office = this.group.getOffice(); } return office; } public Staff getSavingsOfficer() { return this.savingsOfficer; } public void unassignSavingsOfficer() { this.savingsOfficer = null; } public void assignSavingsOfficer(final Staff fieldOfficer) { this.savingsOfficer = fieldOfficer; } public Long clientId() { Long id = null; if (this.client != null) { id = this.client.getId(); } return id; } public Long groupId() { Long id = null; if (this.group != null) { id = this.group.getId(); } return id; } public Long hasSavingsOfficerId() { Long id = null; if (this.savingsOfficer != null) { id = this.savingsOfficer.getId(); } return id; } public boolean hasSavingsOfficer(final Staff fromSavingsOfficer) { boolean matchesCurrentSavingsOfficer = false; if (this.savingsOfficer != null) { matchesCurrentSavingsOfficer = this.savingsOfficer.identifiedBy(fromSavingsOfficer); } else { matchesCurrentSavingsOfficer = fromSavingsOfficer == null; } return matchesCurrentSavingsOfficer; } public void reassignSavingsOfficer(final Staff newSavingsOfficer, final LocalDate assignmentDate) { final SavingsOfficerAssignmentHistory latestHistoryRecord = findLatestIncompleteHistoryRecord(); final SavingsOfficerAssignmentHistory lastAssignmentRecord = findLastAssignmentHistoryRecord( newSavingsOfficer); // assignment date should not be less than savings account submitted // date if (isSubmittedOnDateAfter(assignmentDate)) { final String errorMessage = "The Savings Officer assignment date (" + assignmentDate.toString() + ") cannot be before savings submitted date (" + getSubmittedOnDate().toString() + ")."; throw new SavingsOfficerAssignmentDateException("cannot.be.before.savings.submitted.date", errorMessage, assignmentDate, getSubmittedOnDate()); } else if (lastAssignmentRecord != null && lastAssignmentRecord.isEndDateAfter(assignmentDate)) { final String errorMessage = "The Savings Officer assignment date (" + assignmentDate + ") cannot be before previous Savings Officer unassigned date (" + lastAssignmentRecord.getEndDate() + ")."; throw new SavingsOfficerAssignmentDateException("cannot.be.before.previous.unassignement.date", errorMessage, assignmentDate, lastAssignmentRecord.getEndDate()); } else if (DateUtils.getLocalDateOfTenant().isBefore(assignmentDate)) { final String errorMessage = "The Savings Officer assignment date (" + assignmentDate + ") cannot be in the future."; throw new SavingsOfficerAssignmentDateException("cannot.be.a.future.date", errorMessage, assignmentDate); } else if (latestHistoryRecord != null && this.savingsOfficer.identifiedBy(newSavingsOfficer)) { latestHistoryRecord.updateStartDate(assignmentDate); } else if (latestHistoryRecord != null && latestHistoryRecord.matchesStartDateOf(assignmentDate)) { latestHistoryRecord.updateSavingsOfficer(newSavingsOfficer); this.savingsOfficer = newSavingsOfficer; } else if (latestHistoryRecord != null && latestHistoryRecord.hasStartDateBefore(assignmentDate)) { final String errorMessage = "Savings account with identifier " + getId() + " was already assigned before date " + assignmentDate; throw new SavingsOfficerAssignmentDateException("is.before.last.assignment.date", errorMessage, getId(), assignmentDate); } else { if (latestHistoryRecord != null) { // savings officer correctly changed from previous savings // officer to // new savings officer latestHistoryRecord.updateEndDate(assignmentDate); } this.savingsOfficer = newSavingsOfficer; if (isNotSubmittedAndPendingApproval()) { final SavingsOfficerAssignmentHistory savingsOfficerAssignmentHistory = SavingsOfficerAssignmentHistory .createNew(this, this.savingsOfficer, assignmentDate); this.savingsOfficerHistory.add(savingsOfficerAssignmentHistory); } } } private SavingsOfficerAssignmentHistory findLastAssignmentHistoryRecord(final Staff newSavingsOfficer) { SavingsOfficerAssignmentHistory lastAssignmentRecordLatestEndDate = null; for (final SavingsOfficerAssignmentHistory historyRecord : this.savingsOfficerHistory) { if (historyRecord.isCurrentRecord() && !historyRecord.isSameSavingsOfficer(newSavingsOfficer)) { lastAssignmentRecordLatestEndDate = historyRecord; break; } if (lastAssignmentRecordLatestEndDate == null) { lastAssignmentRecordLatestEndDate = historyRecord; } else if (historyRecord.isEndDateAfter(lastAssignmentRecordLatestEndDate.getEndDate()) && !historyRecord.isSameSavingsOfficer(newSavingsOfficer)) { lastAssignmentRecordLatestEndDate = historyRecord; } } return lastAssignmentRecordLatestEndDate; } public boolean isSubmittedOnDateAfter(final LocalDate compareDate) { return this.submittedOnDate == null ? false : new LocalDate(this.submittedOnDate).isAfter(compareDate); } public LocalDate getSubmittedOnDate() { return (LocalDate) ObjectUtils.defaultIfNull(new LocalDate(this.submittedOnDate), null); } public void removeSavingsOfficer(final LocalDate unassignDate) { final SavingsOfficerAssignmentHistory latestHistoryRecord = findLatestIncompleteHistoryRecord(); if (latestHistoryRecord != null) { validateUnassignDate(latestHistoryRecord, unassignDate); latestHistoryRecord.updateEndDate(unassignDate); } this.savingsOfficer = null; } private SavingsOfficerAssignmentHistory findLatestIncompleteHistoryRecord() { SavingsOfficerAssignmentHistory latestRecordWithNoEndDate = null; for (final SavingsOfficerAssignmentHistory historyRecord : this.savingsOfficerHistory) { if (historyRecord.isCurrentRecord()) { latestRecordWithNoEndDate = historyRecord; break; } } return latestRecordWithNoEndDate; } private void validateUnassignDate(final SavingsOfficerAssignmentHistory latestHistoryRecord, final LocalDate unassignDate) { final LocalDate today = DateUtils.getLocalDateOfTenant(); if (latestHistoryRecord.getStartDate().isAfter(unassignDate)) { final String errorMessage = "The Savings officer Unassign date(" + unassignDate + ") cannot be before its assignment date (" + latestHistoryRecord.getStartDate() + ")."; throw new SavingsOfficerUnassignmentDateException("cannot.be.before.assignment.date", errorMessage, getId(), getSavingsOfficer().getId(), latestHistoryRecord.getStartDate(), unassignDate); } else if (unassignDate.isAfter(today)) { final String errorMessage = "The Savings Officer Unassign date (" + unassignDate + ") cannot be in the future."; throw new SavingsOfficerUnassignmentDateException("cannot.be.a.future.date", errorMessage, unassignDate); } } public MonetaryCurrency getCurrency() { return this.currency; } public void validateNewApplicationState(final LocalDate todayDateOfTenant, final String resourceName) { // validateWithdrawalFeeDetails(); // validateAnnualFeeDetails(); final LocalDate submittedOn = getSubmittedOnLocalDate(); final List<ApiParameterError> dataValidationErrors = new ArrayList<>(); final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) .resource(resourceName + SavingsApiConstants.summitalAction); validateLockinDetails(baseDataValidator); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } if (submittedOn.isAfter(todayDateOfTenant)) { baseDataValidator.reset().parameter(SavingsApiConstants.submittedOnDateParamName).value(submittedOn) .failWithCodeNoParameterAddedToErrorCode("cannot.be.a.future.date"); } if (this.client != null && this.client.isActivatedAfter(submittedOn)) { baseDataValidator.reset().parameter(SavingsApiConstants.submittedOnDateParamName) .value(this.client.getActivationLocalDate()) .failWithCodeNoParameterAddedToErrorCode("cannot.be.before.client.activation.date"); } else if (this.group != null && this.group.isActivatedAfter(submittedOn)) { baseDataValidator.reset().parameter(SavingsApiConstants.submittedOnDateParamName) .value(this.group.getActivationLocalDate()) .failWithCodeNoParameterAddedToErrorCode("cannot.be.before.client.activation.date"); } if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } protected LocalDate getSubmittedOnLocalDate() { LocalDate submittedOn = null; if (this.submittedOnDate != null) { submittedOn = new LocalDate(this.submittedOnDate); } return submittedOn; } private LocalDate getApprovedOnLocalDate() { LocalDate approvedOnLocalDate = null; if (this.approvedOnDate != null) { approvedOnLocalDate = new LocalDate(this.approvedOnDate); } return approvedOnLocalDate; } public Client getClient() { return this.client; } public BigDecimal getNominalAnnualInterestRate() { return this.nominalAnnualInterestRate; } public BigDecimal getNominalAnnualInterestRateOverdraft() { return this.nominalAnnualInterestRateOverdraft; } public Map<String, Object> approveApplication(final AppUser currentUser, final JsonCommand command, final LocalDate tenantsTodayDate) { final Map<String, Object> actualChanges = new LinkedHashMap<>(); final List<ApiParameterError> dataValidationErrors = new ArrayList<>(); final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) .resource(SAVINGS_ACCOUNT_RESOURCE_NAME + SavingsApiConstants.approvalAction); final SavingsAccountStatusType currentStatus = SavingsAccountStatusType.fromInt(this.status); if (!SavingsAccountStatusType.SUBMITTED_AND_PENDING_APPROVAL.hasStateOf(currentStatus)) { baseDataValidator.reset().parameter(SavingsApiConstants.approvedOnDateParamName) .failWithCodeNoParameterAddedToErrorCode("not.in.submittedandpendingapproval.state"); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } this.status = SavingsAccountStatusType.APPROVED.getValue(); actualChanges.put(SavingsApiConstants.statusParamName, SavingsEnumerations.status(this.status)); // only do below if status has changed in the 'approval' case final LocalDate approvedOn = command .localDateValueOfParameterNamed(SavingsApiConstants.approvedOnDateParamName); final String approvedOnDateChange = command .stringValueOfParameterNamed(SavingsApiConstants.approvedOnDateParamName); this.approvedOnDate = approvedOn.toDate(); this.approvedBy = currentUser; actualChanges.put(SavingsApiConstants.localeParamName, command.locale()); actualChanges.put(SavingsApiConstants.dateFormatParamName, command.dateFormat()); actualChanges.put(SavingsApiConstants.approvedOnDateParamName, approvedOnDateChange); final LocalDate submittalDate = getSubmittedOnLocalDate(); if (approvedOn.isBefore(submittalDate)) { final DateTimeFormatter formatter = DateTimeFormat.forPattern(command.dateFormat()) .withLocale(command.extractLocale()); final String submittalDateAsString = formatter.print(submittalDate); baseDataValidator.reset().parameter(SavingsApiConstants.approvedOnDateParamName) .value(submittalDateAsString) .failWithCodeNoParameterAddedToErrorCode("cannot.be.before.submittal.date"); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } if (approvedOn.isAfter(tenantsTodayDate)) { baseDataValidator.reset().parameter(SavingsApiConstants.approvedOnDateParamName) .failWithCodeNoParameterAddedToErrorCode("cannot.be.a.future.date"); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } validateActivityNotBeforeClientOrGroupTransferDate(SavingsEvent.SAVINGS_APPLICATION_APPROVED, approvedOn); // FIXME - kw - support field officer history for savings accounts // if (this.fieldOfficer != null) { // final LoanOfficerAssignmentHistory loanOfficerAssignmentHistory = // LoanOfficerAssignmentHistory.createNew(this, // this.fieldOfficer, approvedOn); // this.loanOfficerHistory.add(loanOfficerAssignmentHistory); // } if (this.savingsOfficer != null) { final SavingsOfficerAssignmentHistory savingsOfficerAssignmentHistory = SavingsOfficerAssignmentHistory .createNew(this, this.savingsOfficer, approvedOn); this.savingsOfficerHistory.add(savingsOfficerAssignmentHistory); } return actualChanges; } public Map<String, Object> undoApplicationApproval() { final Map<String, Object> actualChanges = new LinkedHashMap<>(); final List<ApiParameterError> dataValidationErrors = new ArrayList<>(); final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) .resource(SAVINGS_ACCOUNT_RESOURCE_NAME + SavingsApiConstants.undoApprovalAction); final SavingsAccountStatusType currentStatus = SavingsAccountStatusType.fromInt(this.status); if (!SavingsAccountStatusType.APPROVED.hasStateOf(currentStatus)) { baseDataValidator.reset().parameter(SavingsApiConstants.approvedOnDateParamName) .failWithCodeNoParameterAddedToErrorCode("not.in.approved.state"); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } this.status = SavingsAccountStatusType.SUBMITTED_AND_PENDING_APPROVAL.getValue(); actualChanges.put(SavingsApiConstants.statusParamName, SavingsEnumerations.status(this.status)); this.approvedOnDate = null; this.approvedBy = null; this.rejectedOnDate = null; this.rejectedBy = null; this.withdrawnOnDate = null; this.withdrawnBy = null; this.closedOnDate = null; this.closedBy = null; actualChanges.put(SavingsApiConstants.approvedOnDateParamName, ""); // FIXME - kw - support field officer history for savings accounts // this.loanOfficerHistory.clear(); return actualChanges; } public void undoTransaction(final Long transactionId) { SavingsAccountTransaction transactionToUndo = null; for (final SavingsAccountTransaction transaction : this.transactions) { if (transaction.isIdentifiedBy(transactionId)) { transactionToUndo = transaction; } } if (transactionToUndo == null) { throw new SavingsAccountTransactionNotFoundException(this.getId(), transactionId); } validateAttemptToUndoTransferRelatedTransactions(transactionToUndo); validateActivityNotBeforeClientOrGroupTransferDate(SavingsEvent.SAVINGS_UNDO_TRANSACTION, transactionToUndo.transactionLocalDate()); transactionToUndo.reverse(); if (transactionToUndo.isChargeTransaction() || transactionToUndo.isWaiveCharge()) { // undo charge final Set<SavingsAccountChargePaidBy> chargesPaidBy = transactionToUndo.getSavingsAccountChargesPaid(); for (final SavingsAccountChargePaidBy savingsAccountChargePaidBy : chargesPaidBy) { final SavingsAccountCharge chargeToUndo = savingsAccountChargePaidBy.getSavingsAccountCharge(); if (transactionToUndo.isChargeTransaction()) { chargeToUndo.undoPayment(this.getCurrency(), transactionToUndo.getAmount(this.getCurrency())); } else if (transactionToUndo.isWaiveCharge()) { chargeToUndo.undoWaiver(this.getCurrency(), transactionToUndo.getAmount(this.getCurrency())); } } } } private Date findLatestAnnualFeeTransactionDueDate() { Date nextDueDate = null; LocalDate lastAnnualFeeTransactionDate = null; for (final SavingsAccountTransaction transaction : retreiveOrderedNonInterestPostingTransactions()) { if (transaction.isAnnualFeeAndNotReversed()) { if (lastAnnualFeeTransactionDate == null) { lastAnnualFeeTransactionDate = transaction.transactionLocalDate(); nextDueDate = lastAnnualFeeTransactionDate.toDate(); } if (transaction.transactionLocalDate().isAfter(lastAnnualFeeTransactionDate)) { lastAnnualFeeTransactionDate = transaction.transactionLocalDate(); nextDueDate = lastAnnualFeeTransactionDate.toDate(); } } } return nextDueDate; } public Map<String, Object> rejectApplication(final AppUser currentUser, final JsonCommand command, final LocalDate tenantsTodayDate) { final Map<String, Object> actualChanges = new LinkedHashMap<>(); final List<ApiParameterError> dataValidationErrors = new ArrayList<>(); final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) .resource(SAVINGS_ACCOUNT_RESOURCE_NAME + SavingsApiConstants.rejectAction); final SavingsAccountStatusType currentStatus = SavingsAccountStatusType.fromInt(this.status); if (!SavingsAccountStatusType.SUBMITTED_AND_PENDING_APPROVAL.hasStateOf(currentStatus)) { baseDataValidator.reset().parameter(SavingsApiConstants.rejectedOnDateParamName) .failWithCodeNoParameterAddedToErrorCode("not.in.submittedandpendingapproval.state"); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } this.status = SavingsAccountStatusType.REJECTED.getValue(); actualChanges.put(SavingsApiConstants.statusParamName, SavingsEnumerations.status(this.status)); final LocalDate rejectedOn = command .localDateValueOfParameterNamed(SavingsApiConstants.rejectedOnDateParamName); final String rejectedOnAsString = command .stringValueOfParameterNamed(SavingsApiConstants.rejectedOnDateParamName); this.rejectedOnDate = rejectedOn.toDate(); this.rejectedBy = currentUser; this.withdrawnOnDate = null; this.withdrawnBy = null; this.closedOnDate = rejectedOn.toDate(); this.closedBy = currentUser; actualChanges.put(SavingsApiConstants.localeParamName, command.locale()); actualChanges.put(SavingsApiConstants.dateFormatParamName, command.dateFormat()); actualChanges.put(SavingsApiConstants.rejectedOnDateParamName, rejectedOnAsString); actualChanges.put(SavingsApiConstants.closedOnDateParamName, rejectedOnAsString); final LocalDate submittalDate = getSubmittedOnLocalDate(); if (rejectedOn.isBefore(submittalDate)) { final DateTimeFormatter formatter = DateTimeFormat.forPattern(command.dateFormat()) .withLocale(command.extractLocale()); final String submittalDateAsString = formatter.print(submittalDate); baseDataValidator.reset().parameter(SavingsApiConstants.rejectedOnDateParamName) .value(submittalDateAsString) .failWithCodeNoParameterAddedToErrorCode("cannot.be.before.submittal.date"); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } if (rejectedOn.isAfter(tenantsTodayDate)) { baseDataValidator.reset().parameter(SavingsApiConstants.rejectedOnDateParamName).value(rejectedOn) .failWithCodeNoParameterAddedToErrorCode("cannot.be.a.future.date"); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } validateActivityNotBeforeClientOrGroupTransferDate(SavingsEvent.SAVINGS_APPLICATION_REJECTED, rejectedOn); return actualChanges; } public Map<String, Object> applicantWithdrawsFromApplication(final AppUser currentUser, final JsonCommand command, final LocalDate tenantsTodayDate) { final Map<String, Object> actualChanges = new LinkedHashMap<>(); final List<ApiParameterError> dataValidationErrors = new ArrayList<>(); final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) .resource(SAVINGS_ACCOUNT_RESOURCE_NAME + SavingsApiConstants.withdrawnByApplicantAction); final SavingsAccountStatusType currentStatus = SavingsAccountStatusType.fromInt(this.status); if (!SavingsAccountStatusType.SUBMITTED_AND_PENDING_APPROVAL.hasStateOf(currentStatus)) { baseDataValidator.reset().parameter(SavingsApiConstants.withdrawnOnDateParamName) .failWithCodeNoParameterAddedToErrorCode("not.in.submittedandpendingapproval.state"); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } this.status = SavingsAccountStatusType.WITHDRAWN_BY_APPLICANT.getValue(); actualChanges.put(SavingsApiConstants.statusParamName, SavingsEnumerations.status(this.status)); final LocalDate withdrawnOn = command .localDateValueOfParameterNamed(SavingsApiConstants.withdrawnOnDateParamName); final String withdrawnOnAsString = command .stringValueOfParameterNamed(SavingsApiConstants.withdrawnOnDateParamName); this.rejectedOnDate = null; this.rejectedBy = null; this.withdrawnOnDate = withdrawnOn.toDate(); this.withdrawnBy = currentUser; this.closedOnDate = withdrawnOn.toDate(); this.closedBy = currentUser; actualChanges.put(SavingsApiConstants.localeParamName, command.locale()); actualChanges.put(SavingsApiConstants.dateFormatParamName, command.dateFormat()); actualChanges.put(SavingsApiConstants.withdrawnOnDateParamName, withdrawnOnAsString); actualChanges.put(SavingsApiConstants.closedOnDateParamName, withdrawnOnAsString); final LocalDate submittalDate = getSubmittedOnLocalDate(); if (withdrawnOn.isBefore(submittalDate)) { final DateTimeFormatter formatter = DateTimeFormat.forPattern(command.dateFormat()) .withLocale(command.extractLocale()); final String submittalDateAsString = formatter.print(submittalDate); baseDataValidator.reset().parameter(SavingsApiConstants.withdrawnOnDateParamName) .value(submittalDateAsString) .failWithCodeNoParameterAddedToErrorCode("cannot.be.before.submittal.date"); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } if (withdrawnOn.isAfter(tenantsTodayDate)) { baseDataValidator.reset().parameter(SavingsApiConstants.withdrawnOnDateParamName).value(withdrawnOn) .failWithCodeNoParameterAddedToErrorCode("cannot.be.a.future.date"); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } validateActivityNotBeforeClientOrGroupTransferDate(SavingsEvent.SAVINGS_APPLICATION_WITHDRAWAL_BY_CUSTOMER, withdrawnOn); return actualChanges; } public Map<String, Object> activate(final AppUser currentUser, final JsonCommand command, final LocalDate tenantsTodayDate) { final Map<String, Object> actualChanges = new LinkedHashMap<>(); final List<ApiParameterError> dataValidationErrors = new ArrayList<>(); final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) .resource(depositAccountType().resourceName() + SavingsApiConstants.activateAction); final SavingsAccountStatusType currentStatus = SavingsAccountStatusType.fromInt(this.status); if (!SavingsAccountStatusType.APPROVED.hasStateOf(currentStatus)) { baseDataValidator.reset().parameter(SavingsApiConstants.activatedOnDateParamName) .failWithCodeNoParameterAddedToErrorCode("not.in.approved.state"); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } final Locale locale = command.extractLocale(); final DateTimeFormatter fmt = DateTimeFormat.forPattern(command.dateFormat()).withLocale(locale); final LocalDate activationDate = command .localDateValueOfParameterNamed(SavingsApiConstants.activatedOnDateParamName); this.status = SavingsAccountStatusType.ACTIVE.getValue(); actualChanges.put(SavingsApiConstants.statusParamName, SavingsEnumerations.status(this.status)); actualChanges.put(SavingsApiConstants.localeParamName, command.locale()); actualChanges.put(SavingsApiConstants.dateFormatParamName, command.dateFormat()); actualChanges.put(SavingsApiConstants.activatedOnDateParamName, activationDate.toString(fmt)); this.rejectedOnDate = null; this.rejectedBy = null; this.withdrawnOnDate = null; this.withdrawnBy = null; this.closedOnDate = null; this.closedBy = null; this.activatedOnDate = activationDate.toDate(); this.activatedBy = currentUser; this.lockedInUntilDate = calculateDateAccountIsLockedUntil(getActivationLocalDate()); /* * if (annualFeeSettingsSet()) { * updateToNextAnnualFeeDueDateFrom(getActivationLocalDate()); } */ if (this.client != null && this.client.isActivatedAfter(activationDate)) { final DateTimeFormatter formatter = DateTimeFormat.forPattern(command.dateFormat()) .withLocale(command.extractLocale()); final String dateAsString = formatter.print(this.client.getActivationLocalDate()); baseDataValidator.reset().parameter(SavingsApiConstants.activatedOnDateParamName).value(dateAsString) .failWithCodeNoParameterAddedToErrorCode("cannot.be.before.client.activation.date"); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } if (this.group != null && this.group.isActivatedAfter(activationDate)) { final DateTimeFormatter formatter = DateTimeFormat.forPattern(command.dateFormat()) .withLocale(command.extractLocale()); final String dateAsString = formatter.print(this.client.getActivationLocalDate()); baseDataValidator.reset().parameter(SavingsApiConstants.activatedOnDateParamName).value(dateAsString) .failWithCodeNoParameterAddedToErrorCode("cannot.be.before.group.activation.date"); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } final LocalDate approvalDate = getApprovedOnLocalDate(); if (activationDate.isBefore(approvalDate)) { final DateTimeFormatter formatter = DateTimeFormat.forPattern(command.dateFormat()) .withLocale(command.extractLocale()); final String dateAsString = formatter.print(approvalDate); baseDataValidator.reset().parameter(SavingsApiConstants.activatedOnDateParamName).value(dateAsString) .failWithCodeNoParameterAddedToErrorCode("cannot.be.before.approval.date"); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } if (activationDate.isAfter(tenantsTodayDate)) { baseDataValidator.reset().parameter(SavingsApiConstants.activatedOnDateParamName).value(activationDate) .failWithCodeNoParameterAddedToErrorCode("cannot.be.a.future.date"); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } validateActivityNotBeforeClientOrGroupTransferDate(SavingsEvent.SAVINGS_ACTIVATE, activationDate); return actualChanges; } public void processAccountUponActivation(final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth, final AppUser user) { // update annual fee due date for (SavingsAccountCharge charge : this.charges()) { charge.updateToNextDueDateFrom(getActivationLocalDate()); } // auto pay the activation time charges this.payActivationCharges(isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, user); // TODO : AA add activation charges to actual changes list } public Money activateWithBalance() { return Money.of(this.currency, this.minRequiredOpeningBalance); } public void approveAndActivateApplication(final Date appliedonDate, final AppUser appliedBy) { this.status = SavingsAccountStatusType.ACTIVE.getValue(); this.approvedOnDate = appliedonDate; this.approvedBy = appliedBy; this.rejectedOnDate = null; this.rejectedBy = null; this.withdrawnOnDate = null; this.withdrawnBy = null; this.closedOnDate = null; this.closedBy = null; this.activatedOnDate = appliedonDate; this.activatedBy = appliedBy; this.lockedInUntilDate = calculateDateAccountIsLockedUntil(getActivationLocalDate()); } private void payActivationCharges(final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth, final AppUser user) { boolean isSavingsChargeApplied = false; for (SavingsAccountCharge savingsAccountCharge : this.charges()) { if (savingsAccountCharge.isSavingsActivation()) { isSavingsChargeApplied = true; payCharge(savingsAccountCharge, savingsAccountCharge.getAmountOutstanding(getCurrency()), getActivationLocalDate(), user); } } if (isSavingsChargeApplied) { final MathContext mc = MathContext.DECIMAL64; boolean isInterestTransfer = false; if (this.isBeforeLastPostingPeriod(getActivationLocalDate())) { final LocalDate today = DateUtils.getLocalDateOfTenant(); this.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth); } else { final LocalDate today = DateUtils.getLocalDateOfTenant(); this.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth); } } } public Map<String, Object> close(final AppUser currentUser, final JsonCommand command, final LocalDate tenantsTodayDate) { final Map<String, Object> actualChanges = new LinkedHashMap<>(); final List<ApiParameterError> dataValidationErrors = new ArrayList<>(); final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) .resource(SAVINGS_ACCOUNT_RESOURCE_NAME + SavingsApiConstants.closeAction); final SavingsAccountStatusType currentStatus = SavingsAccountStatusType.fromInt(this.status); if (!SavingsAccountStatusType.ACTIVE.hasStateOf(currentStatus)) { baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("not.in.active.state"); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } final Locale locale = command.extractLocale(); final DateTimeFormatter fmt = DateTimeFormat.forPattern(command.dateFormat()).withLocale(locale); final LocalDate closedDate = command .localDateValueOfParameterNamed(SavingsApiConstants.closedOnDateParamName); if (closedDate.isBefore(getActivationLocalDate())) { baseDataValidator.reset().parameter(SavingsApiConstants.closedOnDateParamName).value(closedDate) .failWithCode("must.be.after.activation.date"); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } if (closedDate.isAfter(tenantsTodayDate)) { baseDataValidator.reset().parameter(SavingsApiConstants.closedOnDateParamName).value(closedDate) .failWithCode("cannot.be.a.future.date"); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } final List<SavingsAccountTransaction> savingsAccountTransactions = retreiveListOfTransactions(); if (savingsAccountTransactions.size() > 0) { final SavingsAccountTransaction accountTransaction = savingsAccountTransactions .get(savingsAccountTransactions.size() - 1); if (accountTransaction.isAfter(closedDate)) { baseDataValidator.reset().parameter(SavingsApiConstants.closedOnDateParamName).value(closedDate) .failWithCode("must.be.after.last.transaction.date"); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } } if (getAccountBalance().doubleValue() != 0) { baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("results.in.balance.not.zero"); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } validateActivityNotBeforeClientOrGroupTransferDate(SavingsEvent.SAVINGS_CLOSE_ACCOUNT, closedDate); this.status = SavingsAccountStatusType.CLOSED.getValue(); actualChanges.put(SavingsApiConstants.statusParamName, SavingsEnumerations.status(this.status)); actualChanges.put(SavingsApiConstants.localeParamName, command.locale()); actualChanges.put(SavingsApiConstants.dateFormatParamName, command.dateFormat()); actualChanges.put(SavingsApiConstants.closedOnDateParamName, closedDate.toString(fmt)); this.rejectedOnDate = null; this.rejectedBy = null; this.withdrawnOnDate = null; this.withdrawnBy = null; this.closedOnDate = closedDate.toDate(); this.closedBy = currentUser; return actualChanges; } protected void validateActivityNotBeforeClientOrGroupTransferDate(final SavingsEvent event, final LocalDate activityDate) { if (this.client != null && this.client.getOfficeJoiningLocalDate() != null) { final LocalDate clientOfficeJoiningDate = this.client.getOfficeJoiningLocalDate(); if (activityDate.isBefore(clientOfficeJoiningDate)) { throw new SavingsActivityPriorToClientTransferException(event.toString(), clientOfficeJoiningDate); } } } private void validateAttemptToUndoTransferRelatedTransactions( final SavingsAccountTransaction savingsAccountTransaction) { if (savingsAccountTransaction.isTransferRelatedTransaction()) { throw new SavingsTransferTransactionsCannotBeUndoneException(savingsAccountTransaction.getId()); } } private Date calculateDateAccountIsLockedUntil(final LocalDate activationLocalDate) { Date lockedInUntilLocalDate = null; final PeriodFrequencyType lockinPeriodFrequencyType = PeriodFrequencyType .fromInt(this.lockinPeriodFrequencyType); switch (lockinPeriodFrequencyType) { case INVALID: break; case DAYS: lockedInUntilLocalDate = activationLocalDate.plusDays(this.lockinPeriodFrequency).toDate(); break; case WEEKS: lockedInUntilLocalDate = activationLocalDate.plusWeeks(this.lockinPeriodFrequency).toDate(); break; case MONTHS: lockedInUntilLocalDate = activationLocalDate.plusMonths(this.lockinPeriodFrequency).toDate(); break; case YEARS: lockedInUntilLocalDate = activationLocalDate.plusYears(this.lockinPeriodFrequency).toDate(); break; } return lockedInUntilLocalDate; } public Group group() { return this.group; } public boolean isWithdrawalFeeApplicableForTransfer() { return this.withdrawalFeeApplicableForTransfer; } public void activateAccountBasedOnBalance() { if (SavingsAccountStatusType.fromInt(this.status).isClosed() && !this.summary.getAccountBalance(getCurrency()).isZero()) { this.status = SavingsAccountStatusType.ACTIVE.getValue(); } } public LocalDate getClosedOnDate() { return (LocalDate) ObjectUtils.defaultIfNull(new LocalDate(this.closedOnDate), null); } public SavingsAccountSummary getSummary() { return this.summary; } public List<SavingsAccountTransaction> getTransactions() { return this.transactions; } public void setStatus(final Integer status) { this.status = status; } private Set<SavingsAccountCharge> associateChargesWithThisSavingsAccount( final Set<SavingsAccountCharge> savingsAccountCharges) { for (final SavingsAccountCharge savingsAccountCharge : savingsAccountCharges) { savingsAccountCharge.update(this); } return savingsAccountCharges; } public boolean update(final Set<SavingsAccountCharge> newSavingsAccountCharges) { if (newSavingsAccountCharges == null) { return false; } if (this.charges == null) { this.charges = new HashSet<>(); } this.charges.clear(); this.charges.addAll(associateChargesWithThisSavingsAccount(newSavingsAccountCharges)); return true; } public boolean hasCurrencyCodeOf(final String matchingCurrencyCode) { if (this.currency == null) { return false; } return this.currency.getCode().equalsIgnoreCase(matchingCurrencyCode); } public void removeCharge(final SavingsAccountCharge charge) { final List<ApiParameterError> dataValidationErrors = new ArrayList<>(); final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) .resource(SAVINGS_ACCOUNT_RESOURCE_NAME); if (isClosed()) { baseDataValidator.reset() .failWithCodeNoParameterAddedToErrorCode("delete.transaction.invalid.account.is.closed"); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } if (isActive() || isApproved()) { baseDataValidator.reset() .failWithCodeNoParameterAddedToErrorCode("delete.transaction.invalid.account.is.active"); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } this.charges.remove(charge); } public void waiveCharge(final Long savingsAccountChargeId, final AppUser user) { final List<ApiParameterError> dataValidationErrors = new ArrayList<>(); final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) .resource(SAVINGS_ACCOUNT_RESOURCE_NAME); if (isClosed()) { baseDataValidator.reset() .failWithCodeNoParameterAddedToErrorCode("transaction.invalid.account.is.closed"); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } if (isNotActive()) { baseDataValidator.reset() .failWithCodeNoParameterAddedToErrorCode("transaction.invalid.account.is.not.active"); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } final SavingsAccountCharge savingsAccountCharge = getCharge(savingsAccountChargeId); if (savingsAccountCharge.isNotActive()) { baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("charge.is.not.active"); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } if (savingsAccountCharge.isWithdrawalFee()) { baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode( "transaction.invalid.waiver.of.withdrawal.fee.not.supported"); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } // validate charge is not already paid or waived if (savingsAccountCharge.isWaived()) { baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode( "transaction.invalid.account.charge.is.already.waived"); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } else if (savingsAccountCharge.isPaid()) { baseDataValidator.reset() .failWithCodeNoParameterAddedToErrorCode("transaction.invalid.account.charge.is.paid"); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } // waive charge final Money amountWaived = savingsAccountCharge.waive(getCurrency()); handleWaiverChargeTransactions(savingsAccountCharge, amountWaived, user); } public void addCharge(final DateTimeFormatter formatter, final SavingsAccountCharge savingsAccountCharge, final Charge chargeDefinition) { final List<ApiParameterError> dataValidationErrors = new ArrayList<>(); final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) .resource(SAVINGS_ACCOUNT_RESOURCE_NAME); if (isClosed()) { baseDataValidator.reset() .failWithCodeNoParameterAddedToErrorCode("transaction.invalid.account.is.closed"); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } if (!hasCurrencyCodeOf(chargeDefinition.getCurrencyCode())) { baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode( "transaction.invalid.account.currency.and.charge.currency.not.same"); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } final LocalDate chargeDueDate = savingsAccountCharge.getDueLocalDate(); if (savingsAccountCharge.isOnSpecifiedDueDate()) { if (getActivationLocalDate() != null && chargeDueDate.isBefore(getActivationLocalDate())) { baseDataValidator.reset().parameter(dueAsOfDateParamName) .value(getActivationLocalDate().toString(formatter)) .failWithCodeNoParameterAddedToErrorCode("before.activationDate"); throw new PlatformApiDataValidationException(dataValidationErrors); } else if (getSubmittedOnLocalDate() != null && chargeDueDate.isBefore(getSubmittedOnLocalDate())) { baseDataValidator.reset().parameter(dueAsOfDateParamName) .value(getSubmittedOnLocalDate().toString(formatter)) .failWithCodeNoParameterAddedToErrorCode("before.submittedOnDate"); throw new PlatformApiDataValidationException(dataValidationErrors); } } if (savingsAccountCharge.isSavingsActivation() && !(isSubmittedAndPendingApproval() || (isApproved() && isNotActive()))) { baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode( "not.valid.account.status.cannot.add.activation.time.charge"); throw new PlatformApiDataValidationException(dataValidationErrors); } // Only one withdrawal fee is supported per account if (savingsAccountCharge.isWithdrawalFee()) { if (this.isWithDrawalFeeExists()) { baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode( "multiple.withdrawal.fee.per.account.not.supported"); throw new PlatformApiDataValidationException(dataValidationErrors); } } // Only one annual fee is supported per account if (savingsAccountCharge.isAnnualFee()) { if (this.isAnnualFeeExists()) { baseDataValidator.reset() .failWithCodeNoParameterAddedToErrorCode("multiple.annual.fee.per.account.not.supported"); throw new PlatformApiDataValidationException(dataValidationErrors); } } if (savingsAccountCharge.isAnnualFee() || savingsAccountCharge.isMonthlyFee() || savingsAccountCharge.isWeeklyFee()) { // update due date if (isActive()) { savingsAccountCharge.updateToNextDueDateFrom(getActivationLocalDate()); } else if (isApproved()) { savingsAccountCharge.updateToNextDueDateFrom(getApprovedOnLocalDate()); } } // activation charge and withdrawal charges not required this validation if (savingsAccountCharge.isOnSpecifiedDueDate()) { validateActivityNotBeforeClientOrGroupTransferDate(SavingsEvent.SAVINGS_APPLY_CHARGE, chargeDueDate); } // add new charge to savings account charges().add(savingsAccountCharge); } private boolean isWithDrawalFeeExists() { for (SavingsAccountCharge charge : this.charges()) { if (charge.isWithdrawalFee()) return true; } return false; } private boolean isAnnualFeeExists() { for (SavingsAccountCharge charge : this.charges()) { if (charge.isAnnualFee()) return true; } return false; } public void payCharge(final SavingsAccountCharge savingsAccountCharge, final BigDecimal amountPaid, final LocalDate transactionDate, final DateTimeFormatter formatter, final AppUser user) { final List<ApiParameterError> dataValidationErrors = new ArrayList<>(); final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) .resource(SAVINGS_ACCOUNT_RESOURCE_NAME); if (isClosed()) { baseDataValidator.reset() .failWithCodeNoParameterAddedToErrorCode("transaction.invalid.account.is.closed"); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } if (isNotActive()) { baseDataValidator.reset() .failWithCodeNoParameterAddedToErrorCode("transaction.invalid.account.is.not.active"); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } if (savingsAccountCharge.isNotActive()) { baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("charge.is.not.active"); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } if (getActivationLocalDate() != null && transactionDate.isBefore(getActivationLocalDate())) { baseDataValidator.reset().parameter(dueAsOfDateParamName) .value(getActivationLocalDate().toString(formatter)) .failWithCodeNoParameterAddedToErrorCode("transaction.before.activationDate"); throw new PlatformApiDataValidationException(dataValidationErrors); } if (DateUtils.isDateInTheFuture(transactionDate)) { baseDataValidator.reset().parameter(dueAsOfDateParamName).value(transactionDate.toString(formatter)) .failWithCodeNoParameterAddedToErrorCode("transaction.is.futureDate"); throw new PlatformApiDataValidationException(dataValidationErrors); } if (savingsAccountCharge.isSavingsActivation()) { baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode( "transaction.not.valid.cannot.pay.activation.time.charge.is.automated"); throw new PlatformApiDataValidationException(dataValidationErrors); } if (savingsAccountCharge.isAnnualFee()) { final LocalDate annualFeeDueDate = savingsAccountCharge.getDueLocalDate(); if (annualFeeDueDate == null) { baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("no.annualfee.settings"); throw new PlatformApiDataValidationException(dataValidationErrors); } if (!annualFeeDueDate.equals(transactionDate)) { baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("invalid.date"); throw new PlatformApiDataValidationException(dataValidationErrors); } Date currentAnnualFeeNextDueDate = findLatestAnnualFeeTransactionDueDate(); if (currentAnnualFeeNextDueDate != null && new LocalDate(currentAnnualFeeNextDueDate).isEqual(transactionDate)) { baseDataValidator.reset().parameter("dueDate").value(transactionDate.toString(formatter)) .failWithCodeNoParameterAddedToErrorCode("transaction.exists.on.date"); throw new PlatformApiDataValidationException(dataValidationErrors); } } // validate charge is not already paid or waived if (savingsAccountCharge.isWaived()) { baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode( "transaction.invalid.account.charge.is.already.waived"); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } else if (savingsAccountCharge.isPaid()) { baseDataValidator.reset() .failWithCodeNoParameterAddedToErrorCode("transaction.invalid.account.charge.is.paid"); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } final Money chargePaid = Money.of(currency, amountPaid); if (!savingsAccountCharge.getAmountOutstanding(getCurrency()).isGreaterThanOrEqualTo(chargePaid)) { baseDataValidator.reset() .failWithCodeNoParameterAddedToErrorCode("transaction.invalid.charge.amount.paid.in.access"); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } this.payCharge(savingsAccountCharge, chargePaid, transactionDate, user); } public void payCharge(final SavingsAccountCharge savingsAccountCharge, final Money amountPaid, final LocalDate transactionDate, final AppUser user) { savingsAccountCharge.pay(getCurrency(), amountPaid); handlePayChargeTransactions(savingsAccountCharge, amountPaid, transactionDate, user); } private void handlePayChargeTransactions(SavingsAccountCharge savingsAccountCharge, Money transactionAmount, final LocalDate transactionDate, final AppUser user) { SavingsAccountTransaction chargeTransaction = null; if (savingsAccountCharge.isWithdrawalFee()) { chargeTransaction = SavingsAccountTransaction.withdrawalFee(this, office(), transactionDate, transactionAmount, user); } else if (savingsAccountCharge.isAnnualFee()) { chargeTransaction = SavingsAccountTransaction.annualFee(this, office(), transactionDate, transactionAmount, user); } else { chargeTransaction = SavingsAccountTransaction.charge(this, office(), transactionDate, transactionAmount, user); } handleChargeTransactions(savingsAccountCharge, chargeTransaction); } private void handleWaiverChargeTransactions(SavingsAccountCharge savingsAccountCharge, Money transactionAmount, AppUser user) { final SavingsAccountTransaction chargeTransaction = SavingsAccountTransaction.waiver(this, office(), DateUtils.getLocalDateOfTenant(), transactionAmount, user); handleChargeTransactions(savingsAccountCharge, chargeTransaction); } private void handleChargeTransactions(final SavingsAccountCharge savingsAccountCharge, final SavingsAccountTransaction transaction) { // Provide a link between transaction and savings charge for which // amount is waived. final SavingsAccountChargePaidBy chargePaidBy = SavingsAccountChargePaidBy.instance(transaction, savingsAccountCharge, transaction.getAmount(this.getCurrency()).getAmount()); transaction.getSavingsAccountChargesPaid().add(chargePaidBy); this.getTransactions().add(transaction); } private SavingsAccountCharge getCharge(final Long savingsAccountChargeId) { SavingsAccountCharge charge = null; for (final SavingsAccountCharge existingCharge : this.charges) { if (existingCharge.getId().equals(savingsAccountChargeId)) { charge = existingCharge; break; } } if (charge == null) { throw new SavingsAccountChargeNotFoundException(savingsAccountChargeId, getId()); } return charge; } public Set<SavingsAccountCharge> charges() { return (this.charges == null) ? new HashSet<SavingsAccountCharge>() : this.charges; } public void validateAccountValuesWithProduct() { final List<ApiParameterError> dataValidationErrors = new ArrayList<>(); final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) .resource(SAVINGS_ACCOUNT_RESOURCE_NAME); if (this.overdraftLimit != null && this.product.overdraftLimit() != null && this.overdraftLimit.compareTo(this.product.overdraftLimit()) == 1) { baseDataValidator.reset().parameter(SavingsApiConstants.overdraftLimitParamName) .value(this.overdraftLimit).failWithCode("cannot.exceed.product.value"); } validateInterestPostingAndCompoundingPeriodTypes(baseDataValidator); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } public boolean allowOverdraft() { return this.allowOverdraft; } public LocalDate accountSubmittedOrActivationDate() { return getActivationLocalDate() == null ? getSubmittedOnLocalDate() : getActivationLocalDate(); } public DepositAccountType depositAccountType() { return DepositAccountType.fromInt(depositType); } protected boolean isTransferInterestToOtherAccount() { return false; } public boolean accountSubmittedAndActivationOnSameDate() { if (getSubmittedOnLocalDate() == null || getActivationLocalDate() == null) { return false; } return getActivationLocalDate().isEqual(getSubmittedOnLocalDate()); } public void validateInterestPostingAndCompoundingPeriodTypes(final DataValidatorBuilder baseDataValidator) { Map<SavingsPostingInterestPeriodType, List<SavingsCompoundingInterestPeriodType>> postingtoCompoundMap = new HashMap<>(); postingtoCompoundMap.put(SavingsPostingInterestPeriodType.MONTHLY, Arrays.asList( new SavingsCompoundingInterestPeriodType[] { SavingsCompoundingInterestPeriodType.DAILY, SavingsCompoundingInterestPeriodType.MONTHLY })); postingtoCompoundMap.put(SavingsPostingInterestPeriodType.QUATERLY, Arrays.asList(new SavingsCompoundingInterestPeriodType[] { SavingsCompoundingInterestPeriodType.DAILY, SavingsCompoundingInterestPeriodType.MONTHLY, SavingsCompoundingInterestPeriodType.QUATERLY })); postingtoCompoundMap.put(SavingsPostingInterestPeriodType.BIANNUAL, Arrays.asList(new SavingsCompoundingInterestPeriodType[] { SavingsCompoundingInterestPeriodType.DAILY, SavingsCompoundingInterestPeriodType.MONTHLY, SavingsCompoundingInterestPeriodType.QUATERLY, SavingsCompoundingInterestPeriodType.BI_ANNUAL })); postingtoCompoundMap.put(SavingsPostingInterestPeriodType.ANNUAL, Arrays.asList(new SavingsCompoundingInterestPeriodType[] { SavingsCompoundingInterestPeriodType.DAILY, SavingsCompoundingInterestPeriodType.MONTHLY, SavingsCompoundingInterestPeriodType.QUATERLY, SavingsCompoundingInterestPeriodType.BI_ANNUAL, SavingsCompoundingInterestPeriodType.ANNUAL })); SavingsPostingInterestPeriodType savingsPostingInterestPeriodType = SavingsPostingInterestPeriodType .fromInt(interestPostingPeriodType); SavingsCompoundingInterestPeriodType savingsCompoundingInterestPeriodType = SavingsCompoundingInterestPeriodType .fromInt(interestCompoundingPeriodType); if (postingtoCompoundMap.get(savingsPostingInterestPeriodType) == null || !postingtoCompoundMap .get(savingsPostingInterestPeriodType).contains(savingsCompoundingInterestPeriodType)) { baseDataValidator.failWithCodeNoParameterAddedToErrorCode( "posting.period.type.is.less.than.compound.period.type", savingsPostingInterestPeriodType.name(), savingsCompoundingInterestPeriodType.name()); } } public boolean allowDeposit() { return true; } public boolean allowWithdrawal() { return true; } public boolean allowModify() { return true; } public boolean isTransactionsAllowed() { return isActive(); } public BigDecimal minBalanceForInterestCalculation() { return this.minBalanceForInterestCalculation; } public void inactivateCharge(SavingsAccountCharge savingsAccountCharge, LocalDate inactivationOnDate) { final List<ApiParameterError> dataValidationErrors = new ArrayList<>(); final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) .resource(SAVINGS_ACCOUNT_RESOURCE_NAME); if (isClosed()) { baseDataValidator.reset() .failWithCodeNoParameterAddedToErrorCode("transaction.invalid.account.is.closed"); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } if (isNotActive()) { baseDataValidator.reset() .failWithCodeNoParameterAddedToErrorCode("transaction.invalid.account.is.not.active"); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } savingsAccountCharge.inactiavateCharge(inactivationOnDate); } public SavingsAccountCharge getUpdatedChargeDetails(SavingsAccountCharge savingsAccountCharge) { for (final SavingsAccountCharge charge : this.charges) { if (charge.equals(savingsAccountCharge)) { savingsAccountCharge = charge; break; } } return savingsAccountCharge; } public String getAccountNumber() { return this.accountNumber; } private Money minRequiredBalanceDerived(final MonetaryCurrency currency) { Money minReqBalance = Money.zero(currency); if (this.enforceMinRequiredBalance) { minReqBalance = minReqBalance.plus(this.minRequiredBalance); } if (this.allowOverdraft) { minReqBalance = minReqBalance.minus(this.overdraftLimit); } return minReqBalance; } public BigDecimal getOnHoldFunds() { return this.onHoldFunds == null ? BigDecimal.ZERO : this.onHoldFunds; } public void holdFunds(BigDecimal onHoldFunds) { this.onHoldFunds = getOnHoldFunds().add(onHoldFunds); } public void releaseFunds(BigDecimal onHoldFunds) { this.onHoldFunds = getOnHoldFunds().subtract(onHoldFunds); } public BigDecimal getWithdrawableBalance() { return getAccountBalance().subtract(minRequiredBalanceDerived(getCurrency()).getAmount()) .subtract(this.getOnHoldFunds()); } }