From ea2dfd789d11e0f4460a1a6d97a41c5ef63579c9 Mon Sep 17 00:00:00 2001 From: Jaanu423 Date: Thu, 21 May 2026 20:57:17 +0530 Subject: [PATCH 01/10] feat: senior citizen interest rate fix for FD/RD accounts + CBS table renames MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Apply senior citizen rate override in FixedDepositAccount and RecurringDepositAccount getEffectiveInterestRateAsFraction() so maturity is calculated at correct rate (e.g. 7.5%) instead of chart slab rate on every event: submit, activate, posting, pre-closure, maturity - Override getEffectiveInterestRateAsFractionAccrual() in both FD/RD to keep accrual consistent with maturity calculation — independent of DB nominalAnnualInterestRate value - Add isSeniorCitizen and seniorCitizenInterestRate fields to SavingsAccount entity with JPA mapping and accessor methods; updateNominalAnnualInterestRate() helper added - Extend DepositsApiConstants whitelist and SavingsAccountAssembler.applyCbsFields() to read and persist senior citizen fields from API request as final override step - Add DepositAccountAssembler early-stage rate override for senior citizen accounts - Liquibase migration 2015: add is_senior_citizen and senior_citizen_interest_rate columns to m_savings_account table - Liquibase migration 0300: rename fd_base_interest_rate tables to CBS naming convention - Update CBS copyright header in FdBaseInterestRateEffective and FdBaseInterestRateSlabHistory Co-Authored-By: Claude Sonnet 4.6 --- .../savings/DepositsApiConstants.java | 1382 ++++----- .../domain/DepositAccountAssembler.java | 12 + .../domain/FdBaseInterestRateEffective.java | 150 +- .../domain/FdBaseInterestRateSlabHistory.java | 152 +- .../savings/domain/FixedDepositAccount.java | 1804 ++++++------ .../domain/RecurringDepositAccount.java | 2554 +++++++++-------- .../domain/SavingsAccountAssembler.java | 1264 ++++---- .../db/changelog/tenant/changelog-tenant.xml | 1 + ...me_fd_base_interest_rate_tables_to_cbs.xml | 40 + .../savings/domain/SavingsAccount.java | 18 + .../savings/parts/module-changelog-master.xml | 81 +- ..._add_senior_citizen_to_savings_account.xml | 27 + 12 files changed, 3820 insertions(+), 3665 deletions(-) create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0300_rename_fd_base_interest_rate_tables_to_cbs.xml create mode 100644 fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2015_add_senior_citizen_to_savings_account.xml diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/DepositsApiConstants.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/DepositsApiConstants.java index ab7ca6828..ea26bb46b 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/DepositsApiConstants.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/DepositsApiConstants.java @@ -1,688 +1,694 @@ -/** - * 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; - -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; -import org.apache.fineract.accounting.common.AccountingConstants.SavingProductAccountingParams; - -public final class DepositsApiConstants { - - private DepositsApiConstants() { - - } - - // Deposit products - public static final String FIXED_DEPOSIT_PRODUCT_RESOURCE_NAME = "fixeddepositproduct"; - public static final String RECURRING_DEPOSIT_PRODUCT_RESOURCE_NAME = "recurringdepositproduct"; - - // Deposit accounts - public static final String FIXED_DEPOSIT_ACCOUNT_RESOURCE_NAME = "fixeddepositaccount"; - public static final String RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME = "recurringdepositaccount"; - - public static final String SAVINGS_ACCOUNT_RESOURCE_NAME = "savingsaccount"; - public static final String SAVINGS_ACCOUNT_TRANSACTION_RESOURCE_NAME = "savingsaccount.transaction"; - public static final String SAVINGS_ACCOUNT_CHARGE_RESOURCE_NAME = "savingsaccountcharge"; - - // Current accounts - public static final String CURRENT_ACCOUNT_RESOURCE_NAME = "currentaccount"; - public static final String CURRENT_PRODUCT_RESOURCE_NAME = "currentproduct"; - public static final String CURRENT_ACCOUNT_CHARGE_RESOURCE_NAME = "currentaccountcharge"; - - // deposit product actions - public static final String summitalAction = ".summital"; - public static final String approvalAction = ".approval"; - public static final String undoApprovalAction = ".undoApproval"; - public static final String rejectAction = ".reject"; - public static final String withdrawnByApplicantAction = ".withdrawnByApplicant"; - public static final String activateAction = ".activate"; - public static final String modifyApplicationAction = ".modify"; - public static final String deleteApplicationAction = ".delete"; - public static final String undoTransactionAction = ".undotransaction"; - public static final String applyAnnualFeeTransactionAction = ".applyannualfee"; - public static final String adjustTransactionAction = ".adjusttransaction"; - public static final String closeAction = ".close"; - public static final String preMatureCloseAction = ".preMatureClose"; - public static final String payChargeTransactionAction = ".paycharge"; - public static final String waiveChargeTransactionAction = ".waivecharge"; - - // command - public static final String COMMAND_UNDO_TRANSACTION = "undo"; - public static final String COMMAND_ADJUST_TRANSACTION = "modify"; - public static final String COMMAND_WAIVE_CHARGE = "waive"; - public static final String COMMAND_PAY_CHARGE = "paycharge"; - public static final String UPDATE_DEPOSIT_AMOUNT = "updateDepositAmount"; - - // general - public static final String localeParamName = "locale"; - public static final String dateFormatParamName = "dateFormat"; - public static final String monthDayFormatParamName = "monthDayFormat"; - - // deposit product and account parameters - public static final String idParamName = "id"; - public static final String accountNoParamName = "accountNo"; - public static final String externalIdParamName = "externalId"; - public static final String statusParamName = "status"; - public static final String clientIdParamName = "clientId"; - public static final String groupIdParamName = "groupId"; - public static final String productIdParamName = "productId"; - public static final String fieldOfficerIdParamName = "fieldOfficerId"; - - public static final String submittedOnDateParamName = "submittedOnDate"; - public static final String rejectedOnDateParamName = "rejectedOnDate"; - public static final String withdrawnOnDateParamName = "withdrawnOnDate"; - public static final String approvedOnDateParamName = "approvedOnDate"; - public static final String activatedOnDateParamName = "activatedOnDate"; - public static final String closedOnDateParamName = "closedOnDate"; - public static final String expectedFirstDepositOnDateParamName = "expectedFirstDepositOnDate"; - - public static final String activeParamName = "active"; - public static final String nameParamName = "name"; - public static final String shortNameParamName = "shortName"; - public static final String descriptionParamName = "description"; - public static final String currencyCodeParamName = "currencyCode"; - public static final String digitsAfterDecimalParamName = "digitsAfterDecimal"; - public static final String inMultiplesOfParamName = "inMultiplesOf"; - public static final String nominalAnnualInterestRateParamName = "nominalAnnualInterestRate"; - public static final String interestCompoundingPeriodTypeParamName = "interestCompoundingPeriodType"; - public static final String interestPostingPeriodTypeParamName = "interestPostingPeriodType"; - public static final String interestCalculationTypeParamName = "interestCalculationType"; - public static final String interestCalculationDaysInYearTypeParamName = "interestCalculationDaysInYearType"; - public static final String lockinPeriodFrequencyParamName = "lockinPeriodFrequency"; - public static final String lockinPeriodFrequencyTypeParamName = "lockinPeriodFrequencyType"; - public static final String feeAmountParamName = "feeAmount";// to be deleted - public static final String feeOnMonthDayParamName = "feeOnMonthDay"; - public static final String feeIntervalParamName = "feeInterval"; - public static final String accountingRuleParamName = "accountingRule"; - public static final String paymentTypeIdParamName = "paymentTypeId"; - public static final String transactionAccountNumberParamName = "accountNumber"; - public static final String checkNumberParamName = "checkNumber"; - public static final String routingCodeParamName = "routingCode"; - public static final String receiptNumberParamName = "receiptNumber"; - public static final String bankNumberParamName = "bankNumber"; - public static final String principalAmountParamName = "principalAmount"; - public static final String annualInterestRateParamName = "annualInterestRate"; - public static final String interestPostingPeriodInMonthsParamName = "interestPostingPeriodInMonths"; - public static final String tenureInMonthsParamName = "tenureInMonths"; - public static final String interestCompoundingPeriodInMonthsParamName = "interestCompoundingPeriodInMonths"; - - // Preclosure parameters - public static final String preClosurePenalApplicableParamName = "preClosurePenalApplicable"; - public static final String preClosurePenalInterestParamName = "preClosurePenalInterest"; - public static final String preClosurePenalInterestOnTypeIdParamName = "preClosurePenalInterestOnTypeId"; - public static final String interestFreePeriodFrequencyType = "interestFreePeriodFrequencyType"; - public static final String preClosurePenalInterestOnType = "preClosurePenalInterestOnType"; - - // term paramters - public static final String minDepositTermParamName = "minDepositTerm"; - public static final String maxDepositTermParamName = "maxDepositTerm"; - public static final String minDepositTermTypeIdParamName = "minDepositTermTypeId"; - public static final String maxDepositTermTypeIdParamName = "maxDepositTermTypeId"; - public static final String minDepositTermType = "minDepositTermType"; - public static final String maxDepositTermType = "maxDepositTermType"; - public static final String inMultiplesOfDepositTermParamName = "inMultiplesOfDepositTerm"; - public static final String inMultiplesOfDepositTermTypeIdParamName = "inMultiplesOfDepositTermTypeId"; - public static final String inMultiplesOfDepositTermType = "inMultiplesOfDepositTermType"; - - public static final String depositAmountParamName = "depositAmount"; - public static final String depositMinAmountParamName = "minDepositAmount"; - public static final String depositMaxAmountParamName = "maxDepositAmount"; - public static final String depositPeriodParamName = "depositPeriod"; - public static final String depositPeriodFrequencyIdParamName = "depositPeriodFrequencyId"; - - // recurring parameters - public static final String mandatoryRecommendedDepositAmountParamName = "mandatoryRecommendedDepositAmount"; - public static final String isMandatoryDepositParamName = "isMandatoryDeposit"; - public static final String allowWithdrawalParamName = "allowWithdrawal"; - public static final String adjustAdvanceTowardsFuturePaymentsParamName = "adjustAdvanceTowardsFuturePayments"; - - public static final String recurringFrequencyTypeParamName = "recurringFrequencyType"; - public static final String recurringFrequencyParamName = "recurringFrequency"; - public static final String isCalendarInheritedParamName = "isCalendarInherited"; - - // transaction parameters - public static final String transactionDateParamName = "transactionDate"; - public static final String transactionAmountParamName = "transactionAmount"; - public static final String paymentDetailDataParamName = "paymentDetailData"; - public static final String runningBalanceParamName = "runningBalance"; - public static final String reversedParamName = "reversed"; - public static final String dateParamName = "date"; - public static final String accountIdParamName = "accountId"; - - // recurring deposits update parameters - public static final String effectiveDateParamName = "effectiveDate"; - - // charges parameters - public static final String chargeIdParamName = "chargeId"; - public static final String chargesParamName = "charges"; - public static final String savingsAccountChargeIdParamName = "savingsAccountChargeId"; - public static final String chargeNameParamName = "name"; - public static final String penaltyParamName = "penalty"; - public static final String chargeTimeTypeParamName = "chargeTimeType"; - public static final String dueAsOfDateParamName = "dueDate"; - public static final String chargeCalculationTypeParamName = "chargeCalculationType"; - public static final String percentageParamName = "percentage"; - public static final String amountPercentageAppliedToParamName = "amountPercentageAppliedTo"; - public static final String currencyParamName = "currency"; - public static final String amountWaivedParamName = "amountWaived"; - public static final String amountWrittenOffParamName = "amountWrittenOff"; - public static final String amountOutstandingParamName = "amountOutstanding"; - public static final String amountOrPercentageParamName = "amountOrPercentage"; - public static final String amountParamName = "amount"; - public static final String isManualTransaction = "isManualTransaction"; - public static final String lienTransaction = "lienTransaction"; - public static final String chargesPaidByData = "chargesPaidByData"; - public static final String amountPaidParamName = "amountPaid"; - public static final String chargeOptionsParamName = "chargeOptions"; - public static final String chargePaymentModeParamName = "chargePaymentMode"; - - public static final String noteParamName = "note"; - public static final String chartsParamName = "charts"; - public static final String chartIdParamName = "chartId"; - - // deposit account associations - public static final String transactions = "transactions"; - public static final String charges = "charges"; - public static final String activeChart = "activeChart"; - - // account closure - public static final String onAccountClosureIdParamName = "onAccountClosureId"; - public static final String transferDescriptionParamName = "transferDescription"; - public static final String toSavingsAccountIdParamName = "toSavingsAccountId"; - public static final String savingsAccounts = "savingsAccounts"; - public static final String maturityInstructionIdParamName = "maturityInstructionId"; - public static final String transferToSavingsIdParamName = "transferToSavingsId"; - - public static final String preMatureCloseOnDateParamName = "preMatureCloseOnDate"; - - public static final String linkedAccountParamName = "linkAccountId"; - public static final String transferInterestToSavingsParamName = "transferInterestToSavings"; - - // template - public static final String chartTemplate = "chartTemplate"; - - /** - * Deposit Product Parameters - */ - private static final Set DEPOSIT_PRODUCT_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(localeParamName, - monthDayFormatParamName, nameParamName, shortNameParamName, descriptionParamName, currencyCodeParamName, - digitsAfterDecimalParamName, inMultiplesOfParamName, nominalAnnualInterestRateParamName, interestCompoundingPeriodTypeParamName, - interestPostingPeriodTypeParamName, interestCalculationTypeParamName, interestCalculationDaysInYearTypeParamName, - lockinPeriodFrequencyParamName, lockinPeriodFrequencyTypeParamName, accountingRuleParamName, chargesParamName, - SavingProductAccountingParams.INCOME_FROM_FEES.getValue(), SavingProductAccountingParams.INCOME_FROM_PENALTIES.getValue(), - SavingProductAccountingParams.INTEREST_ON_SAVINGS.getValue(), - SavingProductAccountingParams.PAYMENT_CHANNEL_FUND_SOURCE_MAPPING.getValue(), - SavingProductAccountingParams.SAVINGS_CONTROL.getValue(), SavingProductAccountingParams.TRANSFERS_SUSPENSE.getValue(), - SavingProductAccountingParams.SAVINGS_REFERENCE.getValue(), SavingProductAccountingParams.FEE_INCOME_ACCOUNT_MAPPING.getValue(), - SavingProductAccountingParams.PENALTY_INCOME_ACCOUNT_MAPPING.getValue(), - SavingProductAccountingParams.INTEREST_PAYABLE.getValue(), SavingProductAccountingParams.PENALTIES_RECEIVABLE.getValue(), - SavingProductAccountingParams.FEES_RECEIVABLE.getValue(), chartsParamName, SavingsApiConstants.withHoldTaxParamName, - SavingsApiConstants.taxGroupIdParamName)); - - private static final Set PRECLOSURE_REQUEST_DATA_PARAMETERS = new HashSet<>( - Arrays.asList(preClosurePenalApplicableParamName, preClosurePenalInterestParamName, preClosurePenalInterestOnTypeIdParamName)); - - private static final Set PRECLOSURE_RESPONSE_DATA_PARAMETERS = new HashSet<>( - Arrays.asList(preClosurePenalApplicableParamName, preClosurePenalInterestParamName, preClosurePenalInterestOnType)); - - private static final Set DEPOSIT_TERM_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(minDepositTermParamName, - maxDepositTermParamName, minDepositTermTypeIdParamName, maxDepositTermTypeIdParamName, inMultiplesOfDepositTermParamName, - inMultiplesOfDepositTermTypeIdParamName, depositAmountParamName, depositMinAmountParamName, depositMaxAmountParamName)); - - private static final Set DEPOSIT_TERM_RESPONSE_DATA_PARAMETERS = new HashSet<>(Arrays.asList(minDepositTermParamName, - maxDepositTermParamName, minDepositTermType, maxDepositTermType, inMultiplesOfDepositTermParamName, - inMultiplesOfDepositTermType, depositAmountParamName, depositMinAmountParamName, depositMaxAmountParamName)); - - private static final Set RECURRING_DETAILS_REQUEST_DATA_PARAMETERS = new HashSet<>( - Arrays.asList(mandatoryRecommendedDepositAmountParamName, isMandatoryDepositParamName, allowWithdrawalParamName, - adjustAdvanceTowardsFuturePaymentsParamName, recurringFrequencyTypeParamName, recurringFrequencyParamName, - isCalendarInheritedParamName)); - - private static final Set RECURRING_DETAILS_RESPONSE_DATA_PARAMETERS = new HashSet<>( - Arrays.asList(mandatoryRecommendedDepositAmountParamName, isMandatoryDepositParamName, allowWithdrawalParamName, - adjustAdvanceTowardsFuturePaymentsParamName, recurringFrequencyTypeParamName, recurringFrequencyParamName, - isCalendarInheritedParamName)); - - private static final Set DEPOSIT_PRECLOSURE_CALCULATION_REQUEST_DATA_PARAMETERS = new HashSet<>( - Arrays.asList(preMatureCloseOnDateParamName)); - - public static final Set FIXED_DEPOSIT_PRODUCT_REQUEST_DATA_PARAMETERS = fixedDepositProductRequestData(); - public static final Set FIXED_DEPOSIT_PRODUCT_RESPONSE_DATA_PARAMETERS = fixedDepositProductResponseData(); - - public static final Set RECURRING_DEPOSIT_PRODUCT_REQUEST_DATA_PARAMETERS = recurringDepositProductRequestData(); - public static final Set RECURRING_DEPOSIT_PRODUCT_RESPONSE_DATA_PARAMETERS = recurringDepositProductResponseData(); - - private static Set fixedDepositProductRequestData() { - final Set fixedDepositRequestData = new HashSet<>(); - fixedDepositRequestData.addAll(DEPOSIT_PRODUCT_REQUEST_DATA_PARAMETERS); - fixedDepositRequestData.addAll(PRECLOSURE_REQUEST_DATA_PARAMETERS); - fixedDepositRequestData.addAll(DEPOSIT_TERM_REQUEST_DATA_PARAMETERS); - fixedDepositRequestData.add(SavingsApiConstants.minBalanceForInterestCalculationParamName); - fixedDepositRequestData.add("accountType"); - fixedDepositRequestData.add("customerSegment"); - fixedDepositRequestData.add("productCode"); - fixedDepositRequestData.add("fdType"); - fixedDepositRequestData.add(SavingsApiConstants.glAccountIdParamName); - fixedDepositRequestData.add("effectiveDate"); - fixedDepositRequestData.add("effectiveTo"); - fixedDepositRequestData.add("endDate"); - fixedDepositRequestData.add("introducerRequired"); - fixedDepositRequestData.add("shareLinkingEnabled"); - fixedDepositRequestData.add("nominationFacility"); - fixedDepositRequestData.add("dormancyAlert"); - fixedDepositRequestData.add(SavingsApiConstants.isDormancyTrackingActiveParamName); - fixedDepositRequestData.add(SavingsApiConstants.daysToInactiveParamName); - fixedDepositRequestData.add(SavingsApiConstants.daysToDormancyParamName); - fixedDepositRequestData.add(SavingsApiConstants.daysToEscheatParamName); - fixedDepositRequestData.add("interestCalculateSelector"); - fixedDepositRequestData.add("averagePeriod"); - fixedDepositRequestData.add("autoPost"); - fixedDepositRequestData.add("frequencyFixDay"); - fixedDepositRequestData.add("lastInterestDate"); - fixedDepositRequestData.add("enableLockinPeriod"); - fixedDepositRequestData.add("balanceFrequency"); - fixedDepositRequestData.add("accountClosedZeroBalanceInDayClosing"); - fixedDepositRequestData.add("autoDebitFacility"); - fixedDepositRequestData.add("loanFacility"); - fixedDepositRequestData.add("prematureWithdrawalOption"); - fixedDepositRequestData.add("tenureFlexibility"); - fixedDepositRequestData.add("lienMarked"); - fixedDepositRequestData.add("withdrawalFeeForTransfers"); - fixedDepositRequestData.add("maturityInstructions"); - fixedDepositRequestData.add("creditMaturityProceeds"); - fixedDepositRequestData.add("penaltyGracePeriodMissedInstallments"); - fixedDepositRequestData.add("panelRequestApplicable"); - fixedDepositRequestData.add("noPenaltyNomineeHeirClosure"); - fixedDepositRequestData.add("deceasedSettlement"); - fixedDepositRequestData.add("allowDebitTxnAfterDueDate"); - fixedDepositRequestData.add("escheatLiabilityId"); - fixedDepositRequestData.add("fdSlabs"); - return fixedDepositRequestData; - } - - private static Set fixedDepositProductResponseData() { - final Set fixedDepositRequestData = new HashSet<>(); - fixedDepositRequestData.addAll(DEPOSIT_PRODUCT_REQUEST_DATA_PARAMETERS); - fixedDepositRequestData.addAll(PRECLOSURE_RESPONSE_DATA_PARAMETERS); - fixedDepositRequestData.addAll(DEPOSIT_TERM_RESPONSE_DATA_PARAMETERS); - fixedDepositRequestData.add(SavingsApiConstants.minBalanceForInterestCalculationParamName); - fixedDepositRequestData.add("interestCalculationTypeOption"); - fixedDepositRequestData.add("periodFrequencyOptions"); - fixedDepositRequestData.add("yesNoOptions"); - fixedDepositRequestData.add("accountTypeOptions"); - fixedDepositRequestData.add("customerSegmentOptions"); - fixedDepositRequestData.add("accountType"); - fixedDepositRequestData.add("customerSegment"); - fixedDepositRequestData.add("productCode"); - fixedDepositRequestData.add("fdType"); - fixedDepositRequestData.add(SavingsApiConstants.glAccountIdParamName); - fixedDepositRequestData.add("glAccountName"); - fixedDepositRequestData.add("effectiveDate"); - fixedDepositRequestData.add("effectiveTo"); - fixedDepositRequestData.add("endDate"); - fixedDepositRequestData.add("introducerRequired"); - fixedDepositRequestData.add("shareLinkingEnabled"); - fixedDepositRequestData.add("nominationFacility"); - fixedDepositRequestData.add("dormancyAlert"); - fixedDepositRequestData.add(SavingsApiConstants.isDormancyTrackingActiveParamName); - fixedDepositRequestData.add(SavingsApiConstants.daysToInactiveParamName); - fixedDepositRequestData.add(SavingsApiConstants.daysToDormancyParamName); - fixedDepositRequestData.add(SavingsApiConstants.daysToEscheatParamName); - fixedDepositRequestData.add("interestCalculateSelector"); - fixedDepositRequestData.add("averagePeriod"); - fixedDepositRequestData.add("autoPost"); - fixedDepositRequestData.add("frequencyFixDay"); - fixedDepositRequestData.add("lastInterestDate"); - fixedDepositRequestData.add("enableLockinPeriod"); - fixedDepositRequestData.add("balanceFrequency"); - fixedDepositRequestData.add("accountClosedZeroBalanceInDayClosing"); - fixedDepositRequestData.add("autoDebitFacility"); - fixedDepositRequestData.add("loanFacility"); - fixedDepositRequestData.add("prematureWithdrawalOption"); - fixedDepositRequestData.add("tenureFlexibility"); - fixedDepositRequestData.add("lienMarked"); - fixedDepositRequestData.add("withdrawalFeeForTransfers"); - fixedDepositRequestData.add("maturityInstructions"); - fixedDepositRequestData.add("creditMaturityProceeds"); - fixedDepositRequestData.add("penaltyGracePeriodMissedInstallments"); - fixedDepositRequestData.add("panelRequestApplicable"); - fixedDepositRequestData.add("noPenaltyNomineeHeirClosure"); - fixedDepositRequestData.add("deceasedSettlement"); - fixedDepositRequestData.add("allowDebitTxnAfterDueDate"); - fixedDepositRequestData.add("accountingMappings"); - fixedDepositRequestData.add("paymentChannelToFundSourceMappings"); - fixedDepositRequestData.add("feeToIncomeAccountMappings"); - fixedDepositRequestData.add("penaltyToIncomeAccountMappings"); - return fixedDepositRequestData; - } - - private static Set recurringDepositProductRequestData() { - final Set recurringDepositRequestData = new HashSet<>(); - recurringDepositRequestData.addAll(DEPOSIT_PRODUCT_REQUEST_DATA_PARAMETERS); - recurringDepositRequestData.addAll(PRECLOSURE_REQUEST_DATA_PARAMETERS); - recurringDepositRequestData.addAll(DEPOSIT_TERM_REQUEST_DATA_PARAMETERS); - recurringDepositRequestData.addAll(RECURRING_DETAILS_REQUEST_DATA_PARAMETERS); - recurringDepositRequestData.add(SavingsApiConstants.minBalanceForInterestCalculationParamName); - // Custom CBS fields — Details step - recurringDepositRequestData.add("accountType"); - recurringDepositRequestData.add("customerSegment"); - recurringDepositRequestData.add("productCode"); - recurringDepositRequestData.add(SavingsApiConstants.glAccountIdParamName); - recurringDepositRequestData.add("effectiveTo"); - recurringDepositRequestData.add("endDate"); - recurringDepositRequestData.add("introducerRequired"); - recurringDepositRequestData.add("shareLinkingEnabled"); - // Custom CBS fields — Currency step - recurringDepositRequestData.add("dormancyAlert"); - recurringDepositRequestData.add(SavingsApiConstants.daysToInactiveParamName); - recurringDepositRequestData.add(SavingsApiConstants.daysToDormancyParamName); - recurringDepositRequestData.add(SavingsApiConstants.daysToEscheatParamName); - recurringDepositRequestData.add("balanceSwapOption"); - recurringDepositRequestData.add("msme"); - recurringDepositRequestData.add("nominationFacility"); - recurringDepositRequestData.add("autoDebitFacility"); - // Custom CBS fields — Terms step - recurringDepositRequestData.add("effectiveDate"); - recurringDepositRequestData.add("interestCalculateSelector"); - recurringDepositRequestData.add("averagePeriod"); - recurringDepositRequestData.add("autoPost"); - recurringDepositRequestData.add("frequencyFixDay"); - recurringDepositRequestData.add("lastInterestDate"); - // Custom CBS fields — Settings step - recurringDepositRequestData.add("loanFacility"); - recurringDepositRequestData.add("prematureWithdrawalOption"); - recurringDepositRequestData.add("tenureFlexibility"); - recurringDepositRequestData.add("lienMarked"); - recurringDepositRequestData.add("withdrawalFeeForTransfers"); - recurringDepositRequestData.add("enableLockinPeriod"); - recurringDepositRequestData.add("maturityInstructions"); - recurringDepositRequestData.add("creditMaturityProceeds"); - recurringDepositRequestData.add("penaltyGracePeriodMissedInstallments"); - recurringDepositRequestData.add("panelRequestApplicable"); - recurringDepositRequestData.add("noPenaltyNomineeHeirClosure"); - recurringDepositRequestData.add("deceasedSettlement"); - recurringDepositRequestData.add("allowDebitTxnAfterDueDate"); - recurringDepositRequestData.add("accountClosedZeroBalanceInDayClosing"); - recurringDepositRequestData.add("escheatLiabilityId"); - // Custom CBS fields — Interest rate slabs (sent inline but ignored server-side; saved separately) - recurringDepositRequestData.add("rdSlabs"); - return recurringDepositRequestData; - } - - private static Set recurringDepositProductResponseData() { - final Set recurringDepositRequestData = new HashSet<>(); - recurringDepositRequestData.addAll(DEPOSIT_PRODUCT_REQUEST_DATA_PARAMETERS); - recurringDepositRequestData.addAll(PRECLOSURE_RESPONSE_DATA_PARAMETERS); - recurringDepositRequestData.addAll(DEPOSIT_TERM_RESPONSE_DATA_PARAMETERS); - recurringDepositRequestData.addAll(RECURRING_DETAILS_RESPONSE_DATA_PARAMETERS); - recurringDepositRequestData.add(SavingsApiConstants.minBalanceForInterestCalculationParamName); - // CBS extended fields — Options (for template=true) - recurringDepositRequestData.add("interestCalculationTypeOption"); - recurringDepositRequestData.add("periodFrequencyOptions"); - recurringDepositRequestData.add("yesNoOptions"); - recurringDepositRequestData.add("accountTypeOptions"); - recurringDepositRequestData.add("customerSegmentOptions"); - // CBS extended fields — Details step - recurringDepositRequestData.add("accountType"); - recurringDepositRequestData.add("customerSegment"); - recurringDepositRequestData.add("productCode"); - recurringDepositRequestData.add(SavingsApiConstants.glAccountIdParamName); - recurringDepositRequestData.add("glAccountName"); - recurringDepositRequestData.add("effectiveDate"); - recurringDepositRequestData.add("effectiveTo"); - recurringDepositRequestData.add("endDate"); - recurringDepositRequestData.add("introducerRequired"); - recurringDepositRequestData.add("shareLinkingEnabled"); - recurringDepositRequestData.add("nominationFacility"); - recurringDepositRequestData.add("dormancyAlert"); - // CBS extended fields — Terms step - recurringDepositRequestData.add("interestCalculateSelector"); - recurringDepositRequestData.add("averagePeriod"); - recurringDepositRequestData.add("autoPost"); - recurringDepositRequestData.add("frequencyFixDay"); - recurringDepositRequestData.add("lastInterestDate"); - // CBS extended fields — Settings step - recurringDepositRequestData.add("enableLockinPeriod"); - recurringDepositRequestData.add("balanceFrequency"); - recurringDepositRequestData.add("accountClosedZeroBalanceInDayClosing"); - recurringDepositRequestData.add("balanceSwapOption"); - recurringDepositRequestData.add("msme"); - recurringDepositRequestData.add("autoDebitFacility"); - recurringDepositRequestData.add("loanFacility"); - recurringDepositRequestData.add("prematureWithdrawalOption"); - recurringDepositRequestData.add("tenureFlexibility"); - recurringDepositRequestData.add("lienMarked"); - recurringDepositRequestData.add("maturityInstructions"); - recurringDepositRequestData.add("creditMaturityProceeds"); - recurringDepositRequestData.add("penaltyGracePeriodMissedInstallments"); - recurringDepositRequestData.add("panelRequestApplicable"); - recurringDepositRequestData.add("noPenaltyNomineeHeirClosure"); - recurringDepositRequestData.add("deceasedSettlement"); - recurringDepositRequestData.add("allowDebitTxnAfterDueDate"); - recurringDepositRequestData.add("escheatLiabilityId"); - recurringDepositRequestData.add("accountingMappings"); - recurringDepositRequestData.add("paymentChannelToFundSourceMappings"); - recurringDepositRequestData.add("feeToIncomeAccountMappings"); - recurringDepositRequestData.add("penaltyToIncomeAccountMappings"); - return recurringDepositRequestData; - } - - /** - * Depost Account parameters - */ - - private static final Set DEPOSIT_ACCOUNT_REQUEST_DATA_PARAMETERS = new HashSet<>( - Arrays.asList(localeParamName, dateFormatParamName, monthDayFormatParamName, accountNoParamName, externalIdParamName, - clientIdParamName, groupIdParamName, productIdParamName, fieldOfficerIdParamName, submittedOnDateParamName, - nominalAnnualInterestRateParamName, interestCompoundingPeriodTypeParamName, interestPostingPeriodTypeParamName, - interestCalculationTypeParamName, interestCalculationDaysInYearTypeParamName, lockinPeriodFrequencyParamName, - lockinPeriodFrequencyTypeParamName, chargesParamName, chartsParamName, depositAmountParamName, depositPeriodParamName, - depositPeriodFrequencyIdParamName, savingsAccounts, expectedFirstDepositOnDateParamName, - SavingsApiConstants.withHoldTaxParamName, maturityInstructionIdParamName, transferToSavingsIdParamName)); - - public static final Set FIXED_DEPOSIT_ACCOUNT_REQUEST_DATA_PARAMETERS = fixedDepositAccountRequestData(); - public static final Set FIXED_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS = fixedDepositAccountResponseData(); - public static final Set FIXED_DEPOSIT_ACCOUNT_INTEREST_CALCULATION_PARAMETERS = fixedDepositInterestCalculationData(); - public static final Set RECURRING_DEPOSIT_ACCOUNT_REQUEST_DATA_PARAMETERS = recurringDepositAccountRequestData(); - public static final Set RECURRING_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS = recurringDepositAccountResponseData(); - - private static Set fixedDepositInterestCalculationData() { - final Set fixedDepositInterestCalculationData = new HashSet<>(); - fixedDepositInterestCalculationData.add(principalAmountParamName); - fixedDepositInterestCalculationData.add(annualInterestRateParamName); - fixedDepositInterestCalculationData.add(tenureInMonthsParamName); - fixedDepositInterestCalculationData.add(interestPostingPeriodInMonthsParamName); - fixedDepositInterestCalculationData.add(interestCompoundingPeriodInMonthsParamName); - - return fixedDepositInterestCalculationData; - - } - - private static Set fixedDepositAccountRequestData() { - final Set fixedDepositRequestData = new HashSet<>(); - fixedDepositRequestData.addAll(DEPOSIT_ACCOUNT_REQUEST_DATA_PARAMETERS); - fixedDepositRequestData.addAll(PRECLOSURE_REQUEST_DATA_PARAMETERS); - fixedDepositRequestData.addAll(DEPOSIT_TERM_REQUEST_DATA_PARAMETERS); - fixedDepositRequestData.add(linkedAccountParamName); - fixedDepositRequestData.add(transferInterestToSavingsParamName); - fixedDepositRequestData.add("accountType"); - fixedDepositRequestData.add("customerSegment"); - fixedDepositRequestData.add(SavingsApiConstants.accountStatusParamName); - fixedDepositRequestData.add(SavingsApiConstants.modeOfOperationParamName); - fixedDepositRequestData.add(SavingsApiConstants.specialCategoryParamName); - fixedDepositRequestData.add(SavingsApiConstants.weakerSectionParamName); - fixedDepositRequestData.add(SavingsApiConstants.guardianNameParamName); - fixedDepositRequestData.add(SavingsApiConstants.guardianCifNumberParamName); - fixedDepositRequestData.add(SavingsApiConstants.guardianRelationshipParamName); - fixedDepositRequestData.add(SavingsApiConstants.guardianClientIdParamName); - fixedDepositRequestData.add(SavingsApiConstants.minorDateOfBirthParamName); - fixedDepositRequestData.add(SavingsApiConstants.introducerIdParamName); - fixedDepositRequestData.add(SavingsApiConstants.introducerNameParamName); - fixedDepositRequestData.add(SavingsApiConstants.introducerCifNumberParamName); - fixedDepositRequestData.add(SavingsApiConstants.nomineeParamName); - fixedDepositRequestData.add("nominees"); - fixedDepositRequestData.add(SavingsApiConstants.linkedAccountIdParamName); - fixedDepositRequestData.add(SavingsApiConstants.linkedAccountTypeParamName); - fixedDepositRequestData.add(SavingsApiConstants.linkedAccountNoParamName); - fixedDepositRequestData.add(SavingsApiConstants.jointHoldersParamName); - fixedDepositRequestData.add(SavingsApiConstants.autoDebitFacilityParamName); - fixedDepositRequestData.add(SavingsApiConstants.nominationFacilityParamName); - return fixedDepositRequestData; - } - - private static Set fixedDepositAccountResponseData() { - final Set fixedDepositResponseData = new HashSet<>(); - fixedDepositResponseData.addAll(DEPOSIT_ACCOUNT_REQUEST_DATA_PARAMETERS); - fixedDepositResponseData.addAll(PRECLOSURE_RESPONSE_DATA_PARAMETERS); - fixedDepositResponseData.addAll(DEPOSIT_TERM_RESPONSE_DATA_PARAMETERS); - fixedDepositResponseData.add(linkedAccountParamName); - fixedDepositResponseData.add(transferInterestToSavingsParamName); - fixedDepositResponseData.add("accountType"); - fixedDepositResponseData.add("customerSegment"); - fixedDepositResponseData.add("accountTypeOptions"); - fixedDepositResponseData.add("customerSegmentOptions"); - fixedDepositResponseData.add("productCode"); - fixedDepositResponseData.add("previewAccountNo"); - fixedDepositResponseData.add(SavingsApiConstants.accountStatusParamName); - fixedDepositResponseData.add(SavingsApiConstants.modeOfOperationParamName); - fixedDepositResponseData.add(SavingsApiConstants.specialCategoryParamName); - fixedDepositResponseData.add(SavingsApiConstants.weakerSectionParamName); - fixedDepositResponseData.add(SavingsApiConstants.guardianNameParamName); - fixedDepositResponseData.add(SavingsApiConstants.guardianCifNumberParamName); - fixedDepositResponseData.add(SavingsApiConstants.guardianRelationshipParamName); - fixedDepositResponseData.add(SavingsApiConstants.guardianClientIdParamName); - fixedDepositResponseData.add(SavingsApiConstants.minorDateOfBirthParamName); - fixedDepositResponseData.add(SavingsApiConstants.introducerIdParamName); - fixedDepositResponseData.add(SavingsApiConstants.introducerNameParamName); - fixedDepositResponseData.add(SavingsApiConstants.introducerCifNumberParamName); - fixedDepositResponseData.add(SavingsApiConstants.nomineeParamName); - fixedDepositResponseData.add(SavingsApiConstants.linkedAccountIdParamName); - fixedDepositResponseData.add(SavingsApiConstants.linkedAccountTypeParamName); - fixedDepositResponseData.add(SavingsApiConstants.linkedAccountNoParamName); - fixedDepositResponseData.add(SavingsApiConstants.jointHoldersParamName); - fixedDepositResponseData.add(SavingsApiConstants.nominationFacilityParamName); - fixedDepositResponseData.add(SavingsApiConstants.autoDebitFacilityParamName); - return fixedDepositResponseData; - } - - private static Set recurringDepositAccountRequestData() { - final Set recurringDepositRequestData = new HashSet<>(); - recurringDepositRequestData.addAll(DEPOSIT_ACCOUNT_REQUEST_DATA_PARAMETERS); - recurringDepositRequestData.addAll(PRECLOSURE_REQUEST_DATA_PARAMETERS); - recurringDepositRequestData.addAll(DEPOSIT_TERM_REQUEST_DATA_PARAMETERS); - recurringDepositRequestData.addAll(RECURRING_DETAILS_REQUEST_DATA_PARAMETERS); - // CBS Account fields - recurringDepositRequestData.add("accountType"); - recurringDepositRequestData.add("customerSegment"); - recurringDepositRequestData.add(SavingsApiConstants.accountStatusParamName); - recurringDepositRequestData.add(SavingsApiConstants.modeOfOperationParamName); - recurringDepositRequestData.add(SavingsApiConstants.specialCategoryParamName); - recurringDepositRequestData.add(SavingsApiConstants.weakerSectionParamName); - recurringDepositRequestData.add(SavingsApiConstants.guardianNameParamName); - recurringDepositRequestData.add(SavingsApiConstants.guardianCifNumberParamName); - recurringDepositRequestData.add(SavingsApiConstants.guardianRelationshipParamName); - recurringDepositRequestData.add(SavingsApiConstants.guardianClientIdParamName); - recurringDepositRequestData.add(SavingsApiConstants.minorDateOfBirthParamName); - recurringDepositRequestData.add(SavingsApiConstants.introducerIdParamName); - recurringDepositRequestData.add(SavingsApiConstants.introducerNameParamName); - recurringDepositRequestData.add(SavingsApiConstants.introducerCifNumberParamName); - recurringDepositRequestData.add(SavingsApiConstants.nomineeParamName); - recurringDepositRequestData.add("nominees"); - recurringDepositRequestData.add(SavingsApiConstants.linkedAccountIdParamName); - recurringDepositRequestData.add(SavingsApiConstants.linkedAccountTypeParamName); - recurringDepositRequestData.add(SavingsApiConstants.linkedAccountNoParamName); - recurringDepositRequestData.add(SavingsApiConstants.jointHoldersParamName); - recurringDepositRequestData.add(SavingsApiConstants.balanceSwapOptionParamName); - recurringDepositRequestData.add(SavingsApiConstants.msmeParamName); - recurringDepositRequestData.add(SavingsApiConstants.autoDebitFacilityParamName); - return recurringDepositRequestData; - } - - private static Set recurringDepositAccountResponseData() { - final Set recurringDepositResponseData = new HashSet<>(); - recurringDepositResponseData.addAll(DEPOSIT_ACCOUNT_REQUEST_DATA_PARAMETERS); - recurringDepositResponseData.addAll(PRECLOSURE_RESPONSE_DATA_PARAMETERS); - recurringDepositResponseData.addAll(DEPOSIT_TERM_RESPONSE_DATA_PARAMETERS); - recurringDepositResponseData.addAll(RECURRING_DETAILS_RESPONSE_DATA_PARAMETERS); - // CBS product fields — included in template response when productId is provided - recurringDepositResponseData.add("accountType"); - recurringDepositResponseData.add("customerSegment"); - recurringDepositResponseData.add("accountTypeOptions"); - recurringDepositResponseData.add("customerSegmentOptions"); - recurringDepositResponseData.add("productCode"); - recurringDepositResponseData.add(SavingsApiConstants.glAccountIdParamName); - recurringDepositResponseData.add("glAccountName"); - recurringDepositResponseData.add("effectiveDate"); - recurringDepositResponseData.add("effectiveTo"); - recurringDepositResponseData.add("endDate"); - recurringDepositResponseData.add("introducerRequired"); - recurringDepositResponseData.add("shareLinkingEnabled"); - recurringDepositResponseData.add("nominationFacility"); - recurringDepositResponseData.add("dormancyAlert"); - recurringDepositResponseData.add("interestCalculateSelector"); - recurringDepositResponseData.add("interestCalculationTypeName"); - recurringDepositResponseData.add("averagePeriod"); - recurringDepositResponseData.add("autoPost"); - recurringDepositResponseData.add("frequencyFixDay"); - recurringDepositResponseData.add("lastInterestDate"); - recurringDepositResponseData.add("enableLockinPeriod"); - recurringDepositResponseData.add("balanceFrequency"); - recurringDepositResponseData.add("accountClosedZeroBalanceInDayClosing"); - recurringDepositResponseData.add("balanceSwapOption"); - recurringDepositResponseData.add("msme"); - recurringDepositResponseData.add("autoDebitFacility"); - recurringDepositResponseData.add("loanFacility"); - recurringDepositResponseData.add("prematureWithdrawalOption"); - recurringDepositResponseData.add("tenureFlexibility"); - recurringDepositResponseData.add("lienMarked"); - recurringDepositResponseData.add("maturityInstructions"); - recurringDepositResponseData.add("creditMaturityProceeds"); - recurringDepositResponseData.add("penaltyGracePeriodMissedInstallments"); - recurringDepositResponseData.add("panelRequestApplicable"); - recurringDepositResponseData.add("noPenaltyNomineeHeirClosure"); - recurringDepositResponseData.add("deceasedSettlement"); - recurringDepositResponseData.add("allowDebitTxnAfterDueDate"); - recurringDepositResponseData.add("accountingMappings"); - recurringDepositResponseData.add("paymentChannelToFundSourceMappings"); - recurringDepositResponseData.add("feeToIncomeAccountMappings"); - recurringDepositResponseData.add("penaltyToIncomeAccountMappings"); - return recurringDepositResponseData; - } - - private static final Set RECURRING_DEPOSIT_TRANSACTION_RESPONSE_DATA_PARAMETERS = new HashSet<>( - Arrays.asList(idParamName, "accountId", accountNoParamName, "currency", "amount", dateParamName, paymentDetailDataParamName, - runningBalanceParamName, reversedParamName)); - - private static final Set SAVINGS_ACCOUNT_ACTIVATION_REQUEST_DATA_PARAMETERS = new HashSet<>( - Arrays.asList(localeParamName, dateFormatParamName, activatedOnDateParamName)); - - private static final Set SAVINGS_ACCOUNT_CHARGES_RESPONSE_DATA_PARAMETERS = new HashSet<>( - Arrays.asList(chargeIdParamName, savingsAccountChargeIdParamName, chargeNameParamName, penaltyParamName, - chargeTimeTypeParamName, dueAsOfDateParamName, chargeCalculationTypeParamName, percentageParamName, - amountPercentageAppliedToParamName, currencyParamName, amountWaivedParamName, amountWrittenOffParamName, - amountOutstandingParamName, amountOrPercentageParamName, amountParamName, amountPaidParamName, chargeOptionsParamName)); - - private static final Set SAVINGS_ACCOUNT_CHARGES_ADD_REQUEST_DATA_PARAMETERS = new HashSet<>( - Arrays.asList(chargeIdParamName, amountParamName, dueAsOfDateParamName, dateFormatParamName, localeParamName, - feeOnMonthDayParamName, monthDayFormatParamName, feeIntervalParamName)); - - private static final Set SAVINGS_ACCOUNT_CHARGES_PAY_CHARGE_REQUEST_DATA_PARAMETERS = new HashSet<>( - Arrays.asList(amountParamName, dueAsOfDateParamName, dateFormatParamName, localeParamName)); -} +/** + * 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; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import org.apache.fineract.accounting.common.AccountingConstants.SavingProductAccountingParams; + +public final class DepositsApiConstants { + + private DepositsApiConstants() { + + } + + // Deposit products + public static final String FIXED_DEPOSIT_PRODUCT_RESOURCE_NAME = "fixeddepositproduct"; + public static final String RECURRING_DEPOSIT_PRODUCT_RESOURCE_NAME = "recurringdepositproduct"; + + // Deposit accounts + public static final String FIXED_DEPOSIT_ACCOUNT_RESOURCE_NAME = "fixeddepositaccount"; + public static final String RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME = "recurringdepositaccount"; + + public static final String SAVINGS_ACCOUNT_RESOURCE_NAME = "savingsaccount"; + public static final String SAVINGS_ACCOUNT_TRANSACTION_RESOURCE_NAME = "savingsaccount.transaction"; + public static final String SAVINGS_ACCOUNT_CHARGE_RESOURCE_NAME = "savingsaccountcharge"; + + // Current accounts + public static final String CURRENT_ACCOUNT_RESOURCE_NAME = "currentaccount"; + public static final String CURRENT_PRODUCT_RESOURCE_NAME = "currentproduct"; + public static final String CURRENT_ACCOUNT_CHARGE_RESOURCE_NAME = "currentaccountcharge"; + + // deposit product actions + public static final String summitalAction = ".summital"; + public static final String approvalAction = ".approval"; + public static final String undoApprovalAction = ".undoApproval"; + public static final String rejectAction = ".reject"; + public static final String withdrawnByApplicantAction = ".withdrawnByApplicant"; + public static final String activateAction = ".activate"; + public static final String modifyApplicationAction = ".modify"; + public static final String deleteApplicationAction = ".delete"; + public static final String undoTransactionAction = ".undotransaction"; + public static final String applyAnnualFeeTransactionAction = ".applyannualfee"; + public static final String adjustTransactionAction = ".adjusttransaction"; + public static final String closeAction = ".close"; + public static final String preMatureCloseAction = ".preMatureClose"; + public static final String payChargeTransactionAction = ".paycharge"; + public static final String waiveChargeTransactionAction = ".waivecharge"; + + // command + public static final String COMMAND_UNDO_TRANSACTION = "undo"; + public static final String COMMAND_ADJUST_TRANSACTION = "modify"; + public static final String COMMAND_WAIVE_CHARGE = "waive"; + public static final String COMMAND_PAY_CHARGE = "paycharge"; + public static final String UPDATE_DEPOSIT_AMOUNT = "updateDepositAmount"; + + // general + public static final String localeParamName = "locale"; + public static final String dateFormatParamName = "dateFormat"; + public static final String monthDayFormatParamName = "monthDayFormat"; + + // deposit product and account parameters + public static final String idParamName = "id"; + public static final String accountNoParamName = "accountNo"; + public static final String externalIdParamName = "externalId"; + public static final String statusParamName = "status"; + public static final String clientIdParamName = "clientId"; + public static final String groupIdParamName = "groupId"; + public static final String productIdParamName = "productId"; + public static final String fieldOfficerIdParamName = "fieldOfficerId"; + + public static final String submittedOnDateParamName = "submittedOnDate"; + public static final String rejectedOnDateParamName = "rejectedOnDate"; + public static final String withdrawnOnDateParamName = "withdrawnOnDate"; + public static final String approvedOnDateParamName = "approvedOnDate"; + public static final String activatedOnDateParamName = "activatedOnDate"; + public static final String closedOnDateParamName = "closedOnDate"; + public static final String expectedFirstDepositOnDateParamName = "expectedFirstDepositOnDate"; + + public static final String activeParamName = "active"; + public static final String nameParamName = "name"; + public static final String shortNameParamName = "shortName"; + public static final String descriptionParamName = "description"; + public static final String currencyCodeParamName = "currencyCode"; + public static final String digitsAfterDecimalParamName = "digitsAfterDecimal"; + public static final String inMultiplesOfParamName = "inMultiplesOf"; + public static final String nominalAnnualInterestRateParamName = "nominalAnnualInterestRate"; + public static final String interestCompoundingPeriodTypeParamName = "interestCompoundingPeriodType"; + public static final String interestPostingPeriodTypeParamName = "interestPostingPeriodType"; + public static final String interestCalculationTypeParamName = "interestCalculationType"; + public static final String interestCalculationDaysInYearTypeParamName = "interestCalculationDaysInYearType"; + public static final String lockinPeriodFrequencyParamName = "lockinPeriodFrequency"; + public static final String lockinPeriodFrequencyTypeParamName = "lockinPeriodFrequencyType"; + public static final String feeAmountParamName = "feeAmount";// to be deleted + public static final String feeOnMonthDayParamName = "feeOnMonthDay"; + public static final String feeIntervalParamName = "feeInterval"; + public static final String accountingRuleParamName = "accountingRule"; + public static final String paymentTypeIdParamName = "paymentTypeId"; + public static final String transactionAccountNumberParamName = "accountNumber"; + public static final String checkNumberParamName = "checkNumber"; + public static final String routingCodeParamName = "routingCode"; + public static final String receiptNumberParamName = "receiptNumber"; + public static final String bankNumberParamName = "bankNumber"; + public static final String principalAmountParamName = "principalAmount"; + public static final String annualInterestRateParamName = "annualInterestRate"; + public static final String interestPostingPeriodInMonthsParamName = "interestPostingPeriodInMonths"; + public static final String isSeniorCitizenParamName = "isSeniorCitizen"; + public static final String seniorCitizenInterestRateParamName = "seniorCitizenInterestRate"; + public static final String tenureInMonthsParamName = "tenureInMonths"; + public static final String interestCompoundingPeriodInMonthsParamName = "interestCompoundingPeriodInMonths"; + + // Preclosure parameters + public static final String preClosurePenalApplicableParamName = "preClosurePenalApplicable"; + public static final String preClosurePenalInterestParamName = "preClosurePenalInterest"; + public static final String preClosurePenalInterestOnTypeIdParamName = "preClosurePenalInterestOnTypeId"; + public static final String interestFreePeriodFrequencyType = "interestFreePeriodFrequencyType"; + public static final String preClosurePenalInterestOnType = "preClosurePenalInterestOnType"; + + // term paramters + public static final String minDepositTermParamName = "minDepositTerm"; + public static final String maxDepositTermParamName = "maxDepositTerm"; + public static final String minDepositTermTypeIdParamName = "minDepositTermTypeId"; + public static final String maxDepositTermTypeIdParamName = "maxDepositTermTypeId"; + public static final String minDepositTermType = "minDepositTermType"; + public static final String maxDepositTermType = "maxDepositTermType"; + public static final String inMultiplesOfDepositTermParamName = "inMultiplesOfDepositTerm"; + public static final String inMultiplesOfDepositTermTypeIdParamName = "inMultiplesOfDepositTermTypeId"; + public static final String inMultiplesOfDepositTermType = "inMultiplesOfDepositTermType"; + + public static final String depositAmountParamName = "depositAmount"; + public static final String depositMinAmountParamName = "minDepositAmount"; + public static final String depositMaxAmountParamName = "maxDepositAmount"; + public static final String depositPeriodParamName = "depositPeriod"; + public static final String depositPeriodFrequencyIdParamName = "depositPeriodFrequencyId"; + + // recurring parameters + public static final String mandatoryRecommendedDepositAmountParamName = "mandatoryRecommendedDepositAmount"; + public static final String isMandatoryDepositParamName = "isMandatoryDeposit"; + public static final String allowWithdrawalParamName = "allowWithdrawal"; + public static final String adjustAdvanceTowardsFuturePaymentsParamName = "adjustAdvanceTowardsFuturePayments"; + + public static final String recurringFrequencyTypeParamName = "recurringFrequencyType"; + public static final String recurringFrequencyParamName = "recurringFrequency"; + public static final String isCalendarInheritedParamName = "isCalendarInherited"; + + // transaction parameters + public static final String transactionDateParamName = "transactionDate"; + public static final String transactionAmountParamName = "transactionAmount"; + public static final String paymentDetailDataParamName = "paymentDetailData"; + public static final String runningBalanceParamName = "runningBalance"; + public static final String reversedParamName = "reversed"; + public static final String dateParamName = "date"; + public static final String accountIdParamName = "accountId"; + + // recurring deposits update parameters + public static final String effectiveDateParamName = "effectiveDate"; + + // charges parameters + public static final String chargeIdParamName = "chargeId"; + public static final String chargesParamName = "charges"; + public static final String savingsAccountChargeIdParamName = "savingsAccountChargeId"; + public static final String chargeNameParamName = "name"; + public static final String penaltyParamName = "penalty"; + public static final String chargeTimeTypeParamName = "chargeTimeType"; + public static final String dueAsOfDateParamName = "dueDate"; + public static final String chargeCalculationTypeParamName = "chargeCalculationType"; + public static final String percentageParamName = "percentage"; + public static final String amountPercentageAppliedToParamName = "amountPercentageAppliedTo"; + public static final String currencyParamName = "currency"; + public static final String amountWaivedParamName = "amountWaived"; + public static final String amountWrittenOffParamName = "amountWrittenOff"; + public static final String amountOutstandingParamName = "amountOutstanding"; + public static final String amountOrPercentageParamName = "amountOrPercentage"; + public static final String amountParamName = "amount"; + public static final String isManualTransaction = "isManualTransaction"; + public static final String lienTransaction = "lienTransaction"; + public static final String chargesPaidByData = "chargesPaidByData"; + public static final String amountPaidParamName = "amountPaid"; + public static final String chargeOptionsParamName = "chargeOptions"; + public static final String chargePaymentModeParamName = "chargePaymentMode"; + + public static final String noteParamName = "note"; + public static final String chartsParamName = "charts"; + public static final String chartIdParamName = "chartId"; + + // deposit account associations + public static final String transactions = "transactions"; + public static final String charges = "charges"; + public static final String activeChart = "activeChart"; + + // account closure + public static final String onAccountClosureIdParamName = "onAccountClosureId"; + public static final String transferDescriptionParamName = "transferDescription"; + public static final String toSavingsAccountIdParamName = "toSavingsAccountId"; + public static final String savingsAccounts = "savingsAccounts"; + public static final String maturityInstructionIdParamName = "maturityInstructionId"; + public static final String transferToSavingsIdParamName = "transferToSavingsId"; + + public static final String preMatureCloseOnDateParamName = "preMatureCloseOnDate"; + + public static final String linkedAccountParamName = "linkAccountId"; + public static final String transferInterestToSavingsParamName = "transferInterestToSavings"; + + // template + public static final String chartTemplate = "chartTemplate"; + + /** + * Deposit Product Parameters + */ + private static final Set DEPOSIT_PRODUCT_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(localeParamName, + monthDayFormatParamName, nameParamName, shortNameParamName, descriptionParamName, currencyCodeParamName, + digitsAfterDecimalParamName, inMultiplesOfParamName, nominalAnnualInterestRateParamName, interestCompoundingPeriodTypeParamName, + interestPostingPeriodTypeParamName, interestCalculationTypeParamName, interestCalculationDaysInYearTypeParamName, + lockinPeriodFrequencyParamName, lockinPeriodFrequencyTypeParamName, accountingRuleParamName, chargesParamName, + SavingProductAccountingParams.INCOME_FROM_FEES.getValue(), SavingProductAccountingParams.INCOME_FROM_PENALTIES.getValue(), + SavingProductAccountingParams.INTEREST_ON_SAVINGS.getValue(), + SavingProductAccountingParams.PAYMENT_CHANNEL_FUND_SOURCE_MAPPING.getValue(), + SavingProductAccountingParams.SAVINGS_CONTROL.getValue(), SavingProductAccountingParams.TRANSFERS_SUSPENSE.getValue(), + SavingProductAccountingParams.SAVINGS_REFERENCE.getValue(), SavingProductAccountingParams.FEE_INCOME_ACCOUNT_MAPPING.getValue(), + SavingProductAccountingParams.PENALTY_INCOME_ACCOUNT_MAPPING.getValue(), + SavingProductAccountingParams.INTEREST_PAYABLE.getValue(), SavingProductAccountingParams.PENALTIES_RECEIVABLE.getValue(), + SavingProductAccountingParams.FEES_RECEIVABLE.getValue(), chartsParamName, SavingsApiConstants.withHoldTaxParamName, + SavingsApiConstants.taxGroupIdParamName)); + + private static final Set PRECLOSURE_REQUEST_DATA_PARAMETERS = new HashSet<>( + Arrays.asList(preClosurePenalApplicableParamName, preClosurePenalInterestParamName, preClosurePenalInterestOnTypeIdParamName)); + + private static final Set PRECLOSURE_RESPONSE_DATA_PARAMETERS = new HashSet<>( + Arrays.asList(preClosurePenalApplicableParamName, preClosurePenalInterestParamName, preClosurePenalInterestOnType)); + + private static final Set DEPOSIT_TERM_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(minDepositTermParamName, + maxDepositTermParamName, minDepositTermTypeIdParamName, maxDepositTermTypeIdParamName, inMultiplesOfDepositTermParamName, + inMultiplesOfDepositTermTypeIdParamName, depositAmountParamName, depositMinAmountParamName, depositMaxAmountParamName)); + + private static final Set DEPOSIT_TERM_RESPONSE_DATA_PARAMETERS = new HashSet<>(Arrays.asList(minDepositTermParamName, + maxDepositTermParamName, minDepositTermType, maxDepositTermType, inMultiplesOfDepositTermParamName, + inMultiplesOfDepositTermType, depositAmountParamName, depositMinAmountParamName, depositMaxAmountParamName)); + + private static final Set RECURRING_DETAILS_REQUEST_DATA_PARAMETERS = new HashSet<>( + Arrays.asList(mandatoryRecommendedDepositAmountParamName, isMandatoryDepositParamName, allowWithdrawalParamName, + adjustAdvanceTowardsFuturePaymentsParamName, recurringFrequencyTypeParamName, recurringFrequencyParamName, + isCalendarInheritedParamName)); + + private static final Set RECURRING_DETAILS_RESPONSE_DATA_PARAMETERS = new HashSet<>( + Arrays.asList(mandatoryRecommendedDepositAmountParamName, isMandatoryDepositParamName, allowWithdrawalParamName, + adjustAdvanceTowardsFuturePaymentsParamName, recurringFrequencyTypeParamName, recurringFrequencyParamName, + isCalendarInheritedParamName)); + + private static final Set DEPOSIT_PRECLOSURE_CALCULATION_REQUEST_DATA_PARAMETERS = new HashSet<>( + Arrays.asList(preMatureCloseOnDateParamName)); + + public static final Set FIXED_DEPOSIT_PRODUCT_REQUEST_DATA_PARAMETERS = fixedDepositProductRequestData(); + public static final Set FIXED_DEPOSIT_PRODUCT_RESPONSE_DATA_PARAMETERS = fixedDepositProductResponseData(); + + public static final Set RECURRING_DEPOSIT_PRODUCT_REQUEST_DATA_PARAMETERS = recurringDepositProductRequestData(); + public static final Set RECURRING_DEPOSIT_PRODUCT_RESPONSE_DATA_PARAMETERS = recurringDepositProductResponseData(); + + private static Set fixedDepositProductRequestData() { + final Set fixedDepositRequestData = new HashSet<>(); + fixedDepositRequestData.addAll(DEPOSIT_PRODUCT_REQUEST_DATA_PARAMETERS); + fixedDepositRequestData.addAll(PRECLOSURE_REQUEST_DATA_PARAMETERS); + fixedDepositRequestData.addAll(DEPOSIT_TERM_REQUEST_DATA_PARAMETERS); + fixedDepositRequestData.add(SavingsApiConstants.minBalanceForInterestCalculationParamName); + fixedDepositRequestData.add("accountType"); + fixedDepositRequestData.add("customerSegment"); + fixedDepositRequestData.add("productCode"); + fixedDepositRequestData.add("fdType"); + fixedDepositRequestData.add(SavingsApiConstants.glAccountIdParamName); + fixedDepositRequestData.add("effectiveDate"); + fixedDepositRequestData.add("effectiveTo"); + fixedDepositRequestData.add("endDate"); + fixedDepositRequestData.add("introducerRequired"); + fixedDepositRequestData.add("shareLinkingEnabled"); + fixedDepositRequestData.add("nominationFacility"); + fixedDepositRequestData.add("dormancyAlert"); + fixedDepositRequestData.add(SavingsApiConstants.isDormancyTrackingActiveParamName); + fixedDepositRequestData.add(SavingsApiConstants.daysToInactiveParamName); + fixedDepositRequestData.add(SavingsApiConstants.daysToDormancyParamName); + fixedDepositRequestData.add(SavingsApiConstants.daysToEscheatParamName); + fixedDepositRequestData.add("interestCalculateSelector"); + fixedDepositRequestData.add("averagePeriod"); + fixedDepositRequestData.add("autoPost"); + fixedDepositRequestData.add("frequencyFixDay"); + fixedDepositRequestData.add("lastInterestDate"); + fixedDepositRequestData.add("enableLockinPeriod"); + fixedDepositRequestData.add("balanceFrequency"); + fixedDepositRequestData.add("accountClosedZeroBalanceInDayClosing"); + fixedDepositRequestData.add("autoDebitFacility"); + fixedDepositRequestData.add("loanFacility"); + fixedDepositRequestData.add("prematureWithdrawalOption"); + fixedDepositRequestData.add("tenureFlexibility"); + fixedDepositRequestData.add("lienMarked"); + fixedDepositRequestData.add("withdrawalFeeForTransfers"); + fixedDepositRequestData.add("maturityInstructions"); + fixedDepositRequestData.add("creditMaturityProceeds"); + fixedDepositRequestData.add("penaltyGracePeriodMissedInstallments"); + fixedDepositRequestData.add("panelRequestApplicable"); + fixedDepositRequestData.add("noPenaltyNomineeHeirClosure"); + fixedDepositRequestData.add("deceasedSettlement"); + fixedDepositRequestData.add("allowDebitTxnAfterDueDate"); + fixedDepositRequestData.add("escheatLiabilityId"); + fixedDepositRequestData.add("fdSlabs"); + return fixedDepositRequestData; + } + + private static Set fixedDepositProductResponseData() { + final Set fixedDepositRequestData = new HashSet<>(); + fixedDepositRequestData.addAll(DEPOSIT_PRODUCT_REQUEST_DATA_PARAMETERS); + fixedDepositRequestData.addAll(PRECLOSURE_RESPONSE_DATA_PARAMETERS); + fixedDepositRequestData.addAll(DEPOSIT_TERM_RESPONSE_DATA_PARAMETERS); + fixedDepositRequestData.add(SavingsApiConstants.minBalanceForInterestCalculationParamName); + fixedDepositRequestData.add("interestCalculationTypeOption"); + fixedDepositRequestData.add("periodFrequencyOptions"); + fixedDepositRequestData.add("yesNoOptions"); + fixedDepositRequestData.add("accountTypeOptions"); + fixedDepositRequestData.add("customerSegmentOptions"); + fixedDepositRequestData.add("accountType"); + fixedDepositRequestData.add("customerSegment"); + fixedDepositRequestData.add("productCode"); + fixedDepositRequestData.add("fdType"); + fixedDepositRequestData.add(SavingsApiConstants.glAccountIdParamName); + fixedDepositRequestData.add("glAccountName"); + fixedDepositRequestData.add("effectiveDate"); + fixedDepositRequestData.add("effectiveTo"); + fixedDepositRequestData.add("endDate"); + fixedDepositRequestData.add("introducerRequired"); + fixedDepositRequestData.add("shareLinkingEnabled"); + fixedDepositRequestData.add("nominationFacility"); + fixedDepositRequestData.add("dormancyAlert"); + fixedDepositRequestData.add(SavingsApiConstants.isDormancyTrackingActiveParamName); + fixedDepositRequestData.add(SavingsApiConstants.daysToInactiveParamName); + fixedDepositRequestData.add(SavingsApiConstants.daysToDormancyParamName); + fixedDepositRequestData.add(SavingsApiConstants.daysToEscheatParamName); + fixedDepositRequestData.add("interestCalculateSelector"); + fixedDepositRequestData.add("averagePeriod"); + fixedDepositRequestData.add("autoPost"); + fixedDepositRequestData.add("frequencyFixDay"); + fixedDepositRequestData.add("lastInterestDate"); + fixedDepositRequestData.add("enableLockinPeriod"); + fixedDepositRequestData.add("balanceFrequency"); + fixedDepositRequestData.add("accountClosedZeroBalanceInDayClosing"); + fixedDepositRequestData.add("autoDebitFacility"); + fixedDepositRequestData.add("loanFacility"); + fixedDepositRequestData.add("prematureWithdrawalOption"); + fixedDepositRequestData.add("tenureFlexibility"); + fixedDepositRequestData.add("lienMarked"); + fixedDepositRequestData.add("withdrawalFeeForTransfers"); + fixedDepositRequestData.add("maturityInstructions"); + fixedDepositRequestData.add("creditMaturityProceeds"); + fixedDepositRequestData.add("penaltyGracePeriodMissedInstallments"); + fixedDepositRequestData.add("panelRequestApplicable"); + fixedDepositRequestData.add("noPenaltyNomineeHeirClosure"); + fixedDepositRequestData.add("deceasedSettlement"); + fixedDepositRequestData.add("allowDebitTxnAfterDueDate"); + fixedDepositRequestData.add("accountingMappings"); + fixedDepositRequestData.add("paymentChannelToFundSourceMappings"); + fixedDepositRequestData.add("feeToIncomeAccountMappings"); + fixedDepositRequestData.add("penaltyToIncomeAccountMappings"); + return fixedDepositRequestData; + } + + private static Set recurringDepositProductRequestData() { + final Set recurringDepositRequestData = new HashSet<>(); + recurringDepositRequestData.addAll(DEPOSIT_PRODUCT_REQUEST_DATA_PARAMETERS); + recurringDepositRequestData.addAll(PRECLOSURE_REQUEST_DATA_PARAMETERS); + recurringDepositRequestData.addAll(DEPOSIT_TERM_REQUEST_DATA_PARAMETERS); + recurringDepositRequestData.addAll(RECURRING_DETAILS_REQUEST_DATA_PARAMETERS); + recurringDepositRequestData.add(SavingsApiConstants.minBalanceForInterestCalculationParamName); + // Custom CBS fields — Details step + recurringDepositRequestData.add("accountType"); + recurringDepositRequestData.add("customerSegment"); + recurringDepositRequestData.add("productCode"); + recurringDepositRequestData.add(SavingsApiConstants.glAccountIdParamName); + recurringDepositRequestData.add("effectiveTo"); + recurringDepositRequestData.add("endDate"); + recurringDepositRequestData.add("introducerRequired"); + recurringDepositRequestData.add("shareLinkingEnabled"); + // Custom CBS fields — Currency step + recurringDepositRequestData.add("dormancyAlert"); + recurringDepositRequestData.add(SavingsApiConstants.daysToInactiveParamName); + recurringDepositRequestData.add(SavingsApiConstants.daysToDormancyParamName); + recurringDepositRequestData.add(SavingsApiConstants.daysToEscheatParamName); + recurringDepositRequestData.add("balanceSwapOption"); + recurringDepositRequestData.add("msme"); + recurringDepositRequestData.add("nominationFacility"); + recurringDepositRequestData.add("autoDebitFacility"); + // Custom CBS fields — Terms step + recurringDepositRequestData.add("effectiveDate"); + recurringDepositRequestData.add("interestCalculateSelector"); + recurringDepositRequestData.add("averagePeriod"); + recurringDepositRequestData.add("autoPost"); + recurringDepositRequestData.add("frequencyFixDay"); + recurringDepositRequestData.add("lastInterestDate"); + // Custom CBS fields — Settings step + recurringDepositRequestData.add("loanFacility"); + recurringDepositRequestData.add("prematureWithdrawalOption"); + recurringDepositRequestData.add("tenureFlexibility"); + recurringDepositRequestData.add("lienMarked"); + recurringDepositRequestData.add("withdrawalFeeForTransfers"); + recurringDepositRequestData.add("enableLockinPeriod"); + recurringDepositRequestData.add("maturityInstructions"); + recurringDepositRequestData.add("creditMaturityProceeds"); + recurringDepositRequestData.add("penaltyGracePeriodMissedInstallments"); + recurringDepositRequestData.add("panelRequestApplicable"); + recurringDepositRequestData.add("noPenaltyNomineeHeirClosure"); + recurringDepositRequestData.add("deceasedSettlement"); + recurringDepositRequestData.add("allowDebitTxnAfterDueDate"); + recurringDepositRequestData.add("accountClosedZeroBalanceInDayClosing"); + recurringDepositRequestData.add("escheatLiabilityId"); + // Custom CBS fields — Interest rate slabs (sent inline but ignored server-side; saved separately) + recurringDepositRequestData.add("rdSlabs"); + return recurringDepositRequestData; + } + + private static Set recurringDepositProductResponseData() { + final Set recurringDepositRequestData = new HashSet<>(); + recurringDepositRequestData.addAll(DEPOSIT_PRODUCT_REQUEST_DATA_PARAMETERS); + recurringDepositRequestData.addAll(PRECLOSURE_RESPONSE_DATA_PARAMETERS); + recurringDepositRequestData.addAll(DEPOSIT_TERM_RESPONSE_DATA_PARAMETERS); + recurringDepositRequestData.addAll(RECURRING_DETAILS_RESPONSE_DATA_PARAMETERS); + recurringDepositRequestData.add(SavingsApiConstants.minBalanceForInterestCalculationParamName); + // CBS extended fields — Options (for template=true) + recurringDepositRequestData.add("interestCalculationTypeOption"); + recurringDepositRequestData.add("periodFrequencyOptions"); + recurringDepositRequestData.add("yesNoOptions"); + recurringDepositRequestData.add("accountTypeOptions"); + recurringDepositRequestData.add("customerSegmentOptions"); + // CBS extended fields — Details step + recurringDepositRequestData.add("accountType"); + recurringDepositRequestData.add("customerSegment"); + recurringDepositRequestData.add("productCode"); + recurringDepositRequestData.add(SavingsApiConstants.glAccountIdParamName); + recurringDepositRequestData.add("glAccountName"); + recurringDepositRequestData.add("effectiveDate"); + recurringDepositRequestData.add("effectiveTo"); + recurringDepositRequestData.add("endDate"); + recurringDepositRequestData.add("introducerRequired"); + recurringDepositRequestData.add("shareLinkingEnabled"); + recurringDepositRequestData.add("nominationFacility"); + recurringDepositRequestData.add("dormancyAlert"); + // CBS extended fields — Terms step + recurringDepositRequestData.add("interestCalculateSelector"); + recurringDepositRequestData.add("averagePeriod"); + recurringDepositRequestData.add("autoPost"); + recurringDepositRequestData.add("frequencyFixDay"); + recurringDepositRequestData.add("lastInterestDate"); + // CBS extended fields — Settings step + recurringDepositRequestData.add("enableLockinPeriod"); + recurringDepositRequestData.add("balanceFrequency"); + recurringDepositRequestData.add("accountClosedZeroBalanceInDayClosing"); + recurringDepositRequestData.add("balanceSwapOption"); + recurringDepositRequestData.add("msme"); + recurringDepositRequestData.add("autoDebitFacility"); + recurringDepositRequestData.add("loanFacility"); + recurringDepositRequestData.add("prematureWithdrawalOption"); + recurringDepositRequestData.add("tenureFlexibility"); + recurringDepositRequestData.add("lienMarked"); + recurringDepositRequestData.add("maturityInstructions"); + recurringDepositRequestData.add("creditMaturityProceeds"); + recurringDepositRequestData.add("penaltyGracePeriodMissedInstallments"); + recurringDepositRequestData.add("panelRequestApplicable"); + recurringDepositRequestData.add("noPenaltyNomineeHeirClosure"); + recurringDepositRequestData.add("deceasedSettlement"); + recurringDepositRequestData.add("allowDebitTxnAfterDueDate"); + recurringDepositRequestData.add("escheatLiabilityId"); + recurringDepositRequestData.add("accountingMappings"); + recurringDepositRequestData.add("paymentChannelToFundSourceMappings"); + recurringDepositRequestData.add("feeToIncomeAccountMappings"); + recurringDepositRequestData.add("penaltyToIncomeAccountMappings"); + return recurringDepositRequestData; + } + + /** + * Depost Account parameters + */ + + private static final Set DEPOSIT_ACCOUNT_REQUEST_DATA_PARAMETERS = new HashSet<>( + Arrays.asList(localeParamName, dateFormatParamName, monthDayFormatParamName, accountNoParamName, externalIdParamName, + clientIdParamName, groupIdParamName, productIdParamName, fieldOfficerIdParamName, submittedOnDateParamName, + nominalAnnualInterestRateParamName, interestCompoundingPeriodTypeParamName, interestPostingPeriodTypeParamName, + interestCalculationTypeParamName, interestCalculationDaysInYearTypeParamName, lockinPeriodFrequencyParamName, + lockinPeriodFrequencyTypeParamName, chargesParamName, chartsParamName, depositAmountParamName, depositPeriodParamName, + depositPeriodFrequencyIdParamName, savingsAccounts, expectedFirstDepositOnDateParamName, + SavingsApiConstants.withHoldTaxParamName, maturityInstructionIdParamName, transferToSavingsIdParamName)); + + public static final Set FIXED_DEPOSIT_ACCOUNT_REQUEST_DATA_PARAMETERS = fixedDepositAccountRequestData(); + public static final Set FIXED_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS = fixedDepositAccountResponseData(); + public static final Set FIXED_DEPOSIT_ACCOUNT_INTEREST_CALCULATION_PARAMETERS = fixedDepositInterestCalculationData(); + public static final Set RECURRING_DEPOSIT_ACCOUNT_REQUEST_DATA_PARAMETERS = recurringDepositAccountRequestData(); + public static final Set RECURRING_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS = recurringDepositAccountResponseData(); + + private static Set fixedDepositInterestCalculationData() { + final Set fixedDepositInterestCalculationData = new HashSet<>(); + fixedDepositInterestCalculationData.add(principalAmountParamName); + fixedDepositInterestCalculationData.add(annualInterestRateParamName); + fixedDepositInterestCalculationData.add(tenureInMonthsParamName); + fixedDepositInterestCalculationData.add(interestPostingPeriodInMonthsParamName); + fixedDepositInterestCalculationData.add(interestCompoundingPeriodInMonthsParamName); + + return fixedDepositInterestCalculationData; + + } + + private static Set fixedDepositAccountRequestData() { + final Set fixedDepositRequestData = new HashSet<>(); + fixedDepositRequestData.addAll(DEPOSIT_ACCOUNT_REQUEST_DATA_PARAMETERS); + fixedDepositRequestData.addAll(PRECLOSURE_REQUEST_DATA_PARAMETERS); + fixedDepositRequestData.addAll(DEPOSIT_TERM_REQUEST_DATA_PARAMETERS); + fixedDepositRequestData.add(linkedAccountParamName); + fixedDepositRequestData.add(transferInterestToSavingsParamName); + fixedDepositRequestData.add("accountType"); + fixedDepositRequestData.add("customerSegment"); + fixedDepositRequestData.add(SavingsApiConstants.accountStatusParamName); + fixedDepositRequestData.add(SavingsApiConstants.modeOfOperationParamName); + fixedDepositRequestData.add(SavingsApiConstants.specialCategoryParamName); + fixedDepositRequestData.add(SavingsApiConstants.weakerSectionParamName); + fixedDepositRequestData.add(SavingsApiConstants.guardianNameParamName); + fixedDepositRequestData.add(SavingsApiConstants.guardianCifNumberParamName); + fixedDepositRequestData.add(SavingsApiConstants.guardianRelationshipParamName); + fixedDepositRequestData.add(SavingsApiConstants.guardianClientIdParamName); + fixedDepositRequestData.add(SavingsApiConstants.minorDateOfBirthParamName); + fixedDepositRequestData.add(SavingsApiConstants.introducerIdParamName); + fixedDepositRequestData.add(SavingsApiConstants.introducerNameParamName); + fixedDepositRequestData.add(SavingsApiConstants.introducerCifNumberParamName); + fixedDepositRequestData.add(SavingsApiConstants.nomineeParamName); + fixedDepositRequestData.add("nominees"); + fixedDepositRequestData.add(SavingsApiConstants.linkedAccountIdParamName); + fixedDepositRequestData.add(SavingsApiConstants.linkedAccountTypeParamName); + fixedDepositRequestData.add(SavingsApiConstants.linkedAccountNoParamName); + fixedDepositRequestData.add(SavingsApiConstants.jointHoldersParamName); + fixedDepositRequestData.add(SavingsApiConstants.autoDebitFacilityParamName); + fixedDepositRequestData.add(SavingsApiConstants.nominationFacilityParamName); + fixedDepositRequestData.add(isSeniorCitizenParamName); + fixedDepositRequestData.add(seniorCitizenInterestRateParamName); + return fixedDepositRequestData; + } + + private static Set fixedDepositAccountResponseData() { + final Set fixedDepositResponseData = new HashSet<>(); + fixedDepositResponseData.addAll(DEPOSIT_ACCOUNT_REQUEST_DATA_PARAMETERS); + fixedDepositResponseData.addAll(PRECLOSURE_RESPONSE_DATA_PARAMETERS); + fixedDepositResponseData.addAll(DEPOSIT_TERM_RESPONSE_DATA_PARAMETERS); + fixedDepositResponseData.add(linkedAccountParamName); + fixedDepositResponseData.add(transferInterestToSavingsParamName); + fixedDepositResponseData.add("accountType"); + fixedDepositResponseData.add("customerSegment"); + fixedDepositResponseData.add("accountTypeOptions"); + fixedDepositResponseData.add("customerSegmentOptions"); + fixedDepositResponseData.add("productCode"); + fixedDepositResponseData.add("previewAccountNo"); + fixedDepositResponseData.add(SavingsApiConstants.accountStatusParamName); + fixedDepositResponseData.add(SavingsApiConstants.modeOfOperationParamName); + fixedDepositResponseData.add(SavingsApiConstants.specialCategoryParamName); + fixedDepositResponseData.add(SavingsApiConstants.weakerSectionParamName); + fixedDepositResponseData.add(SavingsApiConstants.guardianNameParamName); + fixedDepositResponseData.add(SavingsApiConstants.guardianCifNumberParamName); + fixedDepositResponseData.add(SavingsApiConstants.guardianRelationshipParamName); + fixedDepositResponseData.add(SavingsApiConstants.guardianClientIdParamName); + fixedDepositResponseData.add(SavingsApiConstants.minorDateOfBirthParamName); + fixedDepositResponseData.add(SavingsApiConstants.introducerIdParamName); + fixedDepositResponseData.add(SavingsApiConstants.introducerNameParamName); + fixedDepositResponseData.add(SavingsApiConstants.introducerCifNumberParamName); + fixedDepositResponseData.add(SavingsApiConstants.nomineeParamName); + fixedDepositResponseData.add(SavingsApiConstants.linkedAccountIdParamName); + fixedDepositResponseData.add(SavingsApiConstants.linkedAccountTypeParamName); + fixedDepositResponseData.add(SavingsApiConstants.linkedAccountNoParamName); + fixedDepositResponseData.add(SavingsApiConstants.jointHoldersParamName); + fixedDepositResponseData.add(SavingsApiConstants.nominationFacilityParamName); + fixedDepositResponseData.add(SavingsApiConstants.autoDebitFacilityParamName); + return fixedDepositResponseData; + } + + private static Set recurringDepositAccountRequestData() { + final Set recurringDepositRequestData = new HashSet<>(); + recurringDepositRequestData.addAll(DEPOSIT_ACCOUNT_REQUEST_DATA_PARAMETERS); + recurringDepositRequestData.addAll(PRECLOSURE_REQUEST_DATA_PARAMETERS); + recurringDepositRequestData.addAll(DEPOSIT_TERM_REQUEST_DATA_PARAMETERS); + recurringDepositRequestData.addAll(RECURRING_DETAILS_REQUEST_DATA_PARAMETERS); + // CBS Account fields + recurringDepositRequestData.add("accountType"); + recurringDepositRequestData.add("customerSegment"); + recurringDepositRequestData.add(SavingsApiConstants.accountStatusParamName); + recurringDepositRequestData.add(SavingsApiConstants.modeOfOperationParamName); + recurringDepositRequestData.add(SavingsApiConstants.specialCategoryParamName); + recurringDepositRequestData.add(SavingsApiConstants.weakerSectionParamName); + recurringDepositRequestData.add(SavingsApiConstants.guardianNameParamName); + recurringDepositRequestData.add(SavingsApiConstants.guardianCifNumberParamName); + recurringDepositRequestData.add(SavingsApiConstants.guardianRelationshipParamName); + recurringDepositRequestData.add(SavingsApiConstants.guardianClientIdParamName); + recurringDepositRequestData.add(SavingsApiConstants.minorDateOfBirthParamName); + recurringDepositRequestData.add(SavingsApiConstants.introducerIdParamName); + recurringDepositRequestData.add(SavingsApiConstants.introducerNameParamName); + recurringDepositRequestData.add(SavingsApiConstants.introducerCifNumberParamName); + recurringDepositRequestData.add(SavingsApiConstants.nomineeParamName); + recurringDepositRequestData.add("nominees"); + recurringDepositRequestData.add(SavingsApiConstants.linkedAccountIdParamName); + recurringDepositRequestData.add(SavingsApiConstants.linkedAccountTypeParamName); + recurringDepositRequestData.add(SavingsApiConstants.linkedAccountNoParamName); + recurringDepositRequestData.add(SavingsApiConstants.jointHoldersParamName); + recurringDepositRequestData.add(SavingsApiConstants.balanceSwapOptionParamName); + recurringDepositRequestData.add(SavingsApiConstants.msmeParamName); + recurringDepositRequestData.add(SavingsApiConstants.autoDebitFacilityParamName); + recurringDepositRequestData.add(isSeniorCitizenParamName); + recurringDepositRequestData.add(seniorCitizenInterestRateParamName); + return recurringDepositRequestData; + } + + private static Set recurringDepositAccountResponseData() { + final Set recurringDepositResponseData = new HashSet<>(); + recurringDepositResponseData.addAll(DEPOSIT_ACCOUNT_REQUEST_DATA_PARAMETERS); + recurringDepositResponseData.addAll(PRECLOSURE_RESPONSE_DATA_PARAMETERS); + recurringDepositResponseData.addAll(DEPOSIT_TERM_RESPONSE_DATA_PARAMETERS); + recurringDepositResponseData.addAll(RECURRING_DETAILS_RESPONSE_DATA_PARAMETERS); + // CBS product fields — included in template response when productId is provided + recurringDepositResponseData.add("accountType"); + recurringDepositResponseData.add("customerSegment"); + recurringDepositResponseData.add("accountTypeOptions"); + recurringDepositResponseData.add("customerSegmentOptions"); + recurringDepositResponseData.add("productCode"); + recurringDepositResponseData.add(SavingsApiConstants.glAccountIdParamName); + recurringDepositResponseData.add("glAccountName"); + recurringDepositResponseData.add("effectiveDate"); + recurringDepositResponseData.add("effectiveTo"); + recurringDepositResponseData.add("endDate"); + recurringDepositResponseData.add("introducerRequired"); + recurringDepositResponseData.add("shareLinkingEnabled"); + recurringDepositResponseData.add("nominationFacility"); + recurringDepositResponseData.add("dormancyAlert"); + recurringDepositResponseData.add("interestCalculateSelector"); + recurringDepositResponseData.add("interestCalculationTypeName"); + recurringDepositResponseData.add("averagePeriod"); + recurringDepositResponseData.add("autoPost"); + recurringDepositResponseData.add("frequencyFixDay"); + recurringDepositResponseData.add("lastInterestDate"); + recurringDepositResponseData.add("enableLockinPeriod"); + recurringDepositResponseData.add("balanceFrequency"); + recurringDepositResponseData.add("accountClosedZeroBalanceInDayClosing"); + recurringDepositResponseData.add("balanceSwapOption"); + recurringDepositResponseData.add("msme"); + recurringDepositResponseData.add("autoDebitFacility"); + recurringDepositResponseData.add("loanFacility"); + recurringDepositResponseData.add("prematureWithdrawalOption"); + recurringDepositResponseData.add("tenureFlexibility"); + recurringDepositResponseData.add("lienMarked"); + recurringDepositResponseData.add("maturityInstructions"); + recurringDepositResponseData.add("creditMaturityProceeds"); + recurringDepositResponseData.add("penaltyGracePeriodMissedInstallments"); + recurringDepositResponseData.add("panelRequestApplicable"); + recurringDepositResponseData.add("noPenaltyNomineeHeirClosure"); + recurringDepositResponseData.add("deceasedSettlement"); + recurringDepositResponseData.add("allowDebitTxnAfterDueDate"); + recurringDepositResponseData.add("accountingMappings"); + recurringDepositResponseData.add("paymentChannelToFundSourceMappings"); + recurringDepositResponseData.add("feeToIncomeAccountMappings"); + recurringDepositResponseData.add("penaltyToIncomeAccountMappings"); + return recurringDepositResponseData; + } + + private static final Set RECURRING_DEPOSIT_TRANSACTION_RESPONSE_DATA_PARAMETERS = new HashSet<>( + Arrays.asList(idParamName, "accountId", accountNoParamName, "currency", "amount", dateParamName, paymentDetailDataParamName, + runningBalanceParamName, reversedParamName)); + + private static final Set SAVINGS_ACCOUNT_ACTIVATION_REQUEST_DATA_PARAMETERS = new HashSet<>( + Arrays.asList(localeParamName, dateFormatParamName, activatedOnDateParamName)); + + private static final Set SAVINGS_ACCOUNT_CHARGES_RESPONSE_DATA_PARAMETERS = new HashSet<>( + Arrays.asList(chargeIdParamName, savingsAccountChargeIdParamName, chargeNameParamName, penaltyParamName, + chargeTimeTypeParamName, dueAsOfDateParamName, chargeCalculationTypeParamName, percentageParamName, + amountPercentageAppliedToParamName, currencyParamName, amountWaivedParamName, amountWrittenOffParamName, + amountOutstandingParamName, amountOrPercentageParamName, amountParamName, amountPaidParamName, chargeOptionsParamName)); + + private static final Set SAVINGS_ACCOUNT_CHARGES_ADD_REQUEST_DATA_PARAMETERS = new HashSet<>( + Arrays.asList(chargeIdParamName, amountParamName, dueAsOfDateParamName, dateFormatParamName, localeParamName, + feeOnMonthDayParamName, monthDayFormatParamName, feeIntervalParamName)); + + private static final Set SAVINGS_ACCOUNT_CHARGES_PAY_CHARGE_REQUEST_DATA_PARAMETERS = new HashSet<>( + Arrays.asList(amountParamName, dueAsOfDateParamName, dateFormatParamName, localeParamName)); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountAssembler.java index 7055c5d6d..6fde2049b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountAssembler.java @@ -31,6 +31,8 @@ import static org.apache.fineract.portfolio.savings.DepositsApiConstants.deposit import static org.apache.fineract.portfolio.savings.DepositsApiConstants.expectedFirstDepositOnDateParamName; import static org.apache.fineract.portfolio.savings.DepositsApiConstants.isCalendarInheritedParamName; import static org.apache.fineract.portfolio.savings.DepositsApiConstants.isMandatoryDepositParamName; +import static org.apache.fineract.portfolio.savings.DepositsApiConstants.isSeniorCitizenParamName; +import static org.apache.fineract.portfolio.savings.DepositsApiConstants.seniorCitizenInterestRateParamName; import static org.apache.fineract.portfolio.savings.DepositsApiConstants.mandatoryRecommendedDepositAmountParamName; import static org.apache.fineract.portfolio.savings.DepositsApiConstants.maturityInstructionIdParamName; import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transferInterestToSavingsParamName; @@ -230,6 +232,16 @@ public class DepositAccountAssembler { interestRate = product.nominalAnnualInterestRate(); } + // Senior citizen interest rate override: use the higher rate when applicable + if (command.parameterExists(isSeniorCitizenParamName) && command.booleanPrimitiveValueOfParameterNamed(isSeniorCitizenParamName)) { + if (command.parameterExists(seniorCitizenInterestRateParamName)) { + final BigDecimal scRate = command.bigDecimalValueOfParameterNamed(seniorCitizenInterestRateParamName); + if (scRate != null && scRate.compareTo(BigDecimal.ZERO) > 0) { + interestRate = scRate; + } + } + } + SavingsCompoundingInterestPeriodType interestCompoundingPeriodType = null; final Integer interestPeriodTypeValue = command.integerValueOfParameterNamed(interestCompoundingPeriodTypeParamName); if (interestPeriodTypeValue != null) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FdBaseInterestRateEffective.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FdBaseInterestRateEffective.java index 688b29e18..e5891c0f3 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FdBaseInterestRateEffective.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FdBaseInterestRateEffective.java @@ -1,75 +1,75 @@ -/** - * 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 - */ -package org.apache.fineract.portfolio.savings.domain; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.Table; -import java.time.LocalDate; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Entity -@Table(name = "m_fd_base_interest_rate_effective") -@Getter -@NoArgsConstructor -public class FdBaseInterestRateEffective { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(name = "fd_type", length = 20) - private String fdType; - - @Column(name = "version_label") - private String versionLabel; - - @Column(name = "effective_from_date") - private LocalDate effectiveFromDate; - - @Column(name = "effective_to_date") - private LocalDate effectiveToDate; - - @Column(name = "is_active") - private Boolean isActive; - - @Column(name = "remarks") - private String remarks; - - @Column(name = "created_by_id") - private Long createdById; - - @Column(name = "created_date") - private LocalDate createdDate; - - public static FdBaseInterestRateEffective create(String fdType, String versionLabel, LocalDate effectiveFromDate, String remarks, - Long createdById) { - FdBaseInterestRateEffective e = new FdBaseInterestRateEffective(); - e.fdType = fdType; - e.versionLabel = versionLabel; - e.effectiveFromDate = effectiveFromDate; - e.isActive = true; - e.remarks = remarks; - e.createdById = createdById; - e.createdDate = LocalDate.now(); - return e; - } - - public void deactivate(LocalDate toDate) { - this.isActive = false; - this.effectiveToDate = toDate; - } -} +/** + * 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 + */ +package org.apache.fineract.portfolio.savings.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.time.LocalDate; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "m_cbs_base_interest_rate_effective") +@Getter +@NoArgsConstructor +public class FdBaseInterestRateEffective { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "fd_type", length = 20) + private String fdType; + + @Column(name = "version_label") + private String versionLabel; + + @Column(name = "effective_from_date") + private LocalDate effectiveFromDate; + + @Column(name = "effective_to_date") + private LocalDate effectiveToDate; + + @Column(name = "is_active") + private Boolean isActive; + + @Column(name = "remarks") + private String remarks; + + @Column(name = "created_by_id") + private Long createdById; + + @Column(name = "created_date") + private LocalDate createdDate; + + public static FdBaseInterestRateEffective create(String fdType, String versionLabel, LocalDate effectiveFromDate, String remarks, + Long createdById) { + FdBaseInterestRateEffective e = new FdBaseInterestRateEffective(); + e.fdType = fdType; + e.versionLabel = versionLabel; + e.effectiveFromDate = effectiveFromDate; + e.isActive = true; + e.remarks = remarks; + e.createdById = createdById; + e.createdDate = LocalDate.now(); + return e; + } + + public void deactivate(LocalDate toDate) { + this.isActive = false; + this.effectiveToDate = toDate; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FdBaseInterestRateSlabHistory.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FdBaseInterestRateSlabHistory.java index 6a6974660..2b6e842f1 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FdBaseInterestRateSlabHistory.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FdBaseInterestRateSlabHistory.java @@ -1,76 +1,76 @@ -/** - * 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 - */ -package org.apache.fineract.portfolio.savings.domain; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.Table; -import java.math.BigDecimal; -import java.time.LocalDate; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Entity -@Table(name = "m_fd_base_interest_rate_slab_history") -@Getter -@NoArgsConstructor -public class FdBaseInterestRateSlabHistory { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(name = "effective_id") - private Long effectiveId; - - @Column(name = "sr_no") - private Integer srNo; - - @Column(name = "description") - private String description; - - @Column(name = "period_type_enum") - private Integer periodTypeEnum; - - @Column(name = "from_period") - private Integer fromPeriod; - - @Column(name = "to_period") - private Integer toPeriod; - - @Column(name = "annual_interest_rate") - private BigDecimal annualInterestRate; - - @Column(name = "senior_citizen_rate") - private BigDecimal seniorCitizenRate; - - @Column(name = "created_date") - private LocalDate createdDate; - - public static FdBaseInterestRateSlabHistory create(Long effectiveId, Integer srNo, String description, Integer periodTypeEnum, - Integer fromPeriod, Integer toPeriod, BigDecimal annualInterestRate, BigDecimal seniorCitizenRate) { - FdBaseInterestRateSlabHistory s = new FdBaseInterestRateSlabHistory(); - s.effectiveId = effectiveId; - s.srNo = srNo; - s.description = description; - s.periodTypeEnum = periodTypeEnum != null ? periodTypeEnum : 1; - s.fromPeriod = fromPeriod; - s.toPeriod = toPeriod; - s.annualInterestRate = annualInterestRate; - s.seniorCitizenRate = seniorCitizenRate; - s.createdDate = LocalDate.now(); - return s; - } -} +/** + * 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 + */ +package org.apache.fineract.portfolio.savings.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.math.BigDecimal; +import java.time.LocalDate; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "m_cbs_base_interest_rate_slab_history") +@Getter +@NoArgsConstructor +public class FdBaseInterestRateSlabHistory { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "effective_id") + private Long effectiveId; + + @Column(name = "sr_no") + private Integer srNo; + + @Column(name = "description") + private String description; + + @Column(name = "period_type_enum") + private Integer periodTypeEnum; + + @Column(name = "from_period") + private Integer fromPeriod; + + @Column(name = "to_period") + private Integer toPeriod; + + @Column(name = "annual_interest_rate") + private BigDecimal annualInterestRate; + + @Column(name = "senior_citizen_rate") + private BigDecimal seniorCitizenRate; + + @Column(name = "created_date") + private LocalDate createdDate; + + public static FdBaseInterestRateSlabHistory create(Long effectiveId, Integer srNo, String description, Integer periodTypeEnum, + Integer fromPeriod, Integer toPeriod, BigDecimal annualInterestRate, BigDecimal seniorCitizenRate) { + FdBaseInterestRateSlabHistory s = new FdBaseInterestRateSlabHistory(); + s.effectiveId = effectiveId; + s.srNo = srNo; + s.description = description; + s.periodTypeEnum = periodTypeEnum != null ? periodTypeEnum : 1; + s.fromPeriod = fromPeriod; + s.toPeriod = toPeriod; + s.annualInterestRate = annualInterestRate; + s.seniorCitizenRate = seniorCitizenRate; + s.createdDate = LocalDate.now(); + return s; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositAccount.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositAccount.java index 8421e9ca0..688b6feca 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositAccount.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositAccount.java @@ -1,894 +1,910 @@ -/** - * 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.DepositsApiConstants.FIXED_DEPOSIT_ACCOUNT_RESOURCE_NAME; -import static org.apache.fineract.portfolio.savings.DepositsApiConstants.depositPeriodParamName; -import static org.apache.fineract.portfolio.savings.DepositsApiConstants.maxDepositTermParamName; -import static org.apache.fineract.portfolio.savings.DepositsApiConstants.onAccountClosureIdParamName; - -import jakarta.persistence.CascadeType; -import jakarta.persistence.DiscriminatorValue; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.OneToOne; -import jakarta.persistence.Transient; -import java.math.BigDecimal; -import java.math.MathContext; -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; -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.ExternalId; -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.organisation.monetary.domain.Money; -import org.apache.fineract.organisation.staff.domain.Staff; -import org.apache.fineract.portfolio.accountdetails.domain.AccountType; -import org.apache.fineract.portfolio.client.domain.Client; -import org.apache.fineract.portfolio.group.domain.Group; -import org.apache.fineract.portfolio.interestratechart.domain.InterestRateChart; -import org.apache.fineract.portfolio.interestratechart.service.InterestRateChartAssembler; -import org.apache.fineract.portfolio.savings.DepositAccountOnClosureType; -import org.apache.fineract.portfolio.savings.DepositAccountType; -import org.apache.fineract.portfolio.savings.DepositsApiConstants; -import org.apache.fineract.portfolio.savings.PreClosurePenalInterestOnType; -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.domain.interest.PostingPeriod; -import org.apache.fineract.portfolio.savings.domain.interest.SavingsAccountTransactionDetailsForPostingPeriod; -import org.apache.fineract.portfolio.savings.service.SavingsEnumerations; -import org.apache.fineract.useradministration.domain.AppUser; - -@Entity -@DiscriminatorValue("200") -public class FixedDepositAccount extends SavingsAccount { - - @OneToOne(mappedBy = "account", cascade = CascadeType.ALL) - private DepositAccountTermAndPreClosure accountTermAndPreClosure; - - @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "account") - protected DepositAccountInterestRateChart chart; - - @Transient - protected InterestRateChartAssembler chartAssembler; - @Transient - private ConfigurationDomainService configurationDomainService; - - protected FixedDepositAccount() { - // - } - - public static FixedDepositAccount createNewApplicationForSubmittal(final Client client, final Group group, final SavingsProduct product, - final Staff fieldOfficer, final String accountNo, final ExternalId 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 savingsAccountCharges, - final DepositAccountTermAndPreClosure accountTermAndPreClosure, final DepositAccountInterestRateChart chart, - boolean withHoldTax) { - - final SavingsAccountStatusType status = SavingsAccountStatusType.SUBMITTED_AND_PENDING_APPROVAL; - final boolean allowOverdraft = false; - final BigDecimal overdraftLimit = new BigDecimal(0); - FixedDepositAccount account = new FixedDepositAccount(client, group, product, fieldOfficer, accountNo, externalId, status, - accountType, submittedOnDate, submittedBy, interestRate, interestCompoundingPeriodType, interestPostingPeriodType, - interestCalculationType, interestCalculationDaysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency, - lockinPeriodFrequencyType, withdrawalFeeApplicableForTransfer, savingsAccountCharges, accountTermAndPreClosure, chart, - allowOverdraft, overdraftLimit, withHoldTax); - - return account; - } - - private FixedDepositAccount(final Client client, final Group group, final SavingsProduct product, final Staff fieldOfficer, - final String accountNo, final ExternalId 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 savingsAccountCharges, - final DepositAccountTermAndPreClosure accountTermAndPreClosure, DepositAccountInterestRateChart chart, - final boolean allowOverdraft, final BigDecimal overdraftLimit, boolean withHoldTax) { - - super(client, group, product, fieldOfficer, accountNo, externalId, status, accountType, submittedOnDate, submittedBy, - nominalAnnualInterestRate, interestCompoundingPeriodType, interestPostingPeriodType, interestCalculationType, - interestCalculationDaysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType, - withdrawalFeeApplicableForTransfer, savingsAccountCharges, allowOverdraft, overdraftLimit, withHoldTax); - - this.accountTermAndPreClosure = accountTermAndPreClosure; - this.chart = chart; - if (this.chart != null) { - this.chart.updateDepositAccountReference(this); - } - } - - @Override - public void modifyApplication(final JsonCommand command, final Map actualChanges) { - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(FIXED_DEPOSIT_ACCOUNT_RESOURCE_NAME + SavingsApiConstants.modifyApplicationAction); - super.modifyApplication(command, actualChanges, baseDataValidator); - final Map termAndPreClosureChanges = accountTermAndPreClosure.update(command, baseDataValidator); - actualChanges.putAll(termAndPreClosureChanges); - validateDomainRules(baseDataValidator); - super.validateInterestPostingAndCompoundingPeriodTypes(baseDataValidator); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - @Override - protected BigDecimal getEffectiveInterestRateAsFraction(final MathContext mc, final LocalDate interestPostingUpToDate) { - boolean isPreMatureClosure = false; - return getEffectiveInterestRateAsFraction(mc, interestPostingUpToDate, isPreMatureClosure); - } - - protected BigDecimal getEffectiveInterestRateAsFraction(final MathContext mc, final LocalDate interestPostingUpToDate, - final boolean isPreMatureClosure) { - - // default it to nominalAnnualInterst rate. interest chart overrrides - // this value. - BigDecimal applicableInterestRate = this.nominalAnnualInterestRate; - if (this.chart != null) { - boolean applyPreMaturePenalty = false; - BigDecimal penalInterest = BigDecimal.ZERO; - LocalDate depositCloseDate = calculateMaturityDate(); - if (isPreMatureClosure) { - if (this.accountTermAndPreClosure.isPreClosurePenalApplicable()) { - applyPreMaturePenalty = true; - penalInterest = this.accountTermAndPreClosure.depositPreClosureDetail().preClosurePenalInterest(); - final PreClosurePenalInterestOnType preClosurePenalInterestOnType = this.accountTermAndPreClosure - .depositPreClosureDetail().preClosurePenalInterestOnType(); - if (preClosurePenalInterestOnType.isWholeTerm()) { - depositCloseDate = interestCalculatedUpto(); - } else if (preClosurePenalInterestOnType.isTillPrematureWithdrawal()) { - depositCloseDate = interestPostingUpToDate; - } - } - } - - final BigDecimal depositAmount = accountTermAndPreClosure.depositAmount(); - applicableInterestRate = this.chart.getApplicableInterestRate(depositAmount, depositStartDate(), depositCloseDate, this.client); - - if (applyPreMaturePenalty) { - applicableInterestRate = applicableInterestRate.subtract(penalInterest); - applicableInterestRate = applicableInterestRate.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : applicableInterestRate; - } - } - this.nominalAnnualInterestRate = applicableInterestRate; - - return applicableInterestRate.divide(BigDecimal.valueOf(100L), mc); - } - - public void updateMaturityDateAndAmountBeforeAccountActivation(final MathContext mc, final boolean isPreMatureClosure, - final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth) { - List allTransactions = new ArrayList<>(); - String refNo = null; - final Money transactionAmountMoney = Money.of(getCurrency(), this.accountTermAndPreClosure.depositAmount()); - final SavingsAccountTransaction transaction = SavingsAccountTransaction.deposit(null, office(), null, - this.accountSubmittedOrActivationDate(), transactionAmountMoney, refNo); - transaction.setRunningBalance(transactionAmountMoney); - transaction.updateCumulativeBalanceAndDates(this.getCurrency(), interestCalculatedUpto()); - allTransactions.add(transaction); - updateMaturityDateAndAmount(mc, allTransactions, isPreMatureClosure, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth); - } - - public void updateMaturityDateAndAmount(final MathContext mc, final boolean isPreMatureClosure, - final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth) { - updateMaturityDateAndAmount(mc, retreiveOrderedNonInterestPostingTransactions(), isPreMatureClosure, - isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth); - } - - public void updateMaturityDateAndAmount(final MathContext mc, final List transactions, - final boolean isPreMatureClosure, final boolean isSavingsInterestPostingAtCurrentPeriodEnd, - final Integer financialYearBeginningMonth) { - final LocalDate maturityDate = calculateMaturityDate(); - final LocalDate interestCalculationUpto = maturityDate.minusDays(1); - - // set end of day balance to maturity date for maturity interest - // calculation - this.resetAccountTransactionsEndOfDayBalances(transactions, maturityDate); - - final List postingPeriods = calculateInterestPayable(mc, interestCalculationUpto, transactions, isPreMatureClosure, - isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth); - - // reset end of day balance back to today's date - this.resetAccountTransactionsEndOfDayBalances(transactions, DateUtils.getBusinessLocalDate()); - - Money totalInterestPayable = Money.zero(getCurrency()); - for (PostingPeriod postingPeriod : postingPeriods) { - totalInterestPayable = totalInterestPayable.plus(postingPeriod.getInterestEarned()); - } - final Money depositAmount = Money.of(getCurrency(), this.accountTermAndPreClosure.depositAmount()); - final Money maturityAmount = depositAmount.plus(totalInterestPayable); - - this.accountTermAndPreClosure.updateMaturityDetails(maturityAmount.getAmount(), maturityDate); - } - - public void updateMaturityStatus(final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth) { - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(FIXED_DEPOSIT_ACCOUNT_RESOURCE_NAME + SavingsApiConstants.updateMaturityDetailsAction); - - 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); - } - } - - if (!DateUtils.isDateInTheFuture(maturityDate())) { - // update account status - this.status = SavingsAccountStatusType.MATURED.getValue(); - postMaturityInterest(isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth); - } - } - - public LocalDate calculateMaturityDate() { - - final LocalDate startDate = accountSubmittedOrActivationDate(); - LocalDate maturityDate = null; - final Integer depositPeriod = this.accountTermAndPreClosure.depositPeriod(); - switch (this.accountTermAndPreClosure.depositPeriodFrequencyType()) { - case DAYS: - maturityDate = startDate.plusDays(depositPeriod); - break; - case WEEKS: - maturityDate = startDate.plusWeeks(depositPeriod); - break; - case MONTHS: - maturityDate = startDate.plusMonths(depositPeriod); - break; - case YEARS: - maturityDate = startDate.plusYears(depositPeriod); - break; - case INVALID: - break; - } - - return maturityDate; - } - - private List calculateInterestPayable(final MathContext mc, final LocalDate maturityDate, - final List transactions, final boolean isPreMatureClosure, - final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth) { - - final SavingsPostingInterestPeriodType postingPeriodType = SavingsPostingInterestPeriodType.fromInt(this.interestPostingPeriodType); - - final SavingsCompoundingInterestPeriodType compoundingPeriodType = SavingsCompoundingInterestPeriodType - .fromInt(this.interestCompoundingPeriodType); - - final SavingsInterestCalculationDaysInYearType daysInYearType = SavingsInterestCalculationDaysInYearType - .fromInt(this.interestCalculationDaysInYearType); - List postedAsOnTransactionDates = getManualPostingDates(); - - final List postingPeriodIntervals = this.savingsHelper.determineInterestPostingPeriods( - accountSubmittedOrActivationDate(), maturityDate, postingPeriodType, financialYearBeginningMonth, - postedAsOnTransactionDates); - - final List allPostingPeriods = new ArrayList<>(); - - Money periodStartingBalance = Money.zero(currency); - - final SavingsInterestCalculationType interestCalculationType = SavingsInterestCalculationType.fromInt(this.interestCalculationType); - final BigDecimal interestRateAsFraction = getEffectiveInterestRateAsFraction(mc, maturityDate, isPreMatureClosure); - final Collection interestPostTransactions = this.savingsHelper.fetchPostInterestTransactionIds(getId()); - boolean isInterestTransfer = false; - final Money minBalanceForInterestCalculation = Money.of(getCurrency(), minBalanceForInterestCalculation()); - List savingsAccountTransactionDetailsForPostingPeriodList = toSavingsAccountTransactionDetailsForPostingPeriodList( - transactions); - for (final LocalDateInterval periodInterval : postingPeriodIntervals) { - boolean isUserPosting = false; - if (postedAsOnTransactionDates.contains(periodInterval.endDate())) { - isUserPosting = true; - } - final PostingPeriod postingPeriod = PostingPeriod.createFrom(periodInterval, periodStartingBalance, - savingsAccountTransactionDetailsForPostingPeriodList, this.currency, compoundingPeriodType, interestCalculationType, - interestRateAsFraction, daysInYearType.getValue(), maturityDate, interestPostTransactions, isInterestTransfer, - minBalanceForInterestCalculation, isSavingsInterestPostingAtCurrentPeriodEnd, isUserPosting, - financialYearBeginningMonth); - - periodStartingBalance = postingPeriod.closingBalance(); - - allPostingPeriods.add(postingPeriod); - } - - this.summary.updateFromInterestPeriodSummaries(this.currency, allPostingPeriods); - this.savingsHelper.calculateInterestForAllPostingPeriods(this.currency, allPostingPeriods, getLockedInUntilDate(), - isTransferInterestToOtherAccount()); - return allPostingPeriods; - } - - public void prematureClosure(final AppUser currentUser, final JsonCommand command, final Map actualChanges) { - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(FIXED_DEPOSIT_ACCOUNT_RESOURCE_NAME + DepositsApiConstants.preMatureCloseAction); - - 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 = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); - final LocalDate closedDate = command.localDateValueOfParameterNamed(SavingsApiConstants.closedOnDateParamName); - - if (DateUtils.isBefore(closedDate, getActivationDate())) { - baseDataValidator.reset().parameter(SavingsApiConstants.closedOnDateParamName).value(closedDate) - .failWithCode("must.be.after.activation.date"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - if (isAccountLocked(closedDate)) { - baseDataValidator.reset().parameter(SavingsApiConstants.closedOnDateParamName).value(closedDate) - .failWithCode("must.be.after.lockin.period"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - if (maturityDate() != null && DateUtils.isAfter(closedDate, maturityDate())) { - baseDataValidator.reset().parameter(SavingsApiConstants.closedOnDateParamName).value(closedDate) - .failWithCode("must.be.before.maturity.date"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - if (DateUtils.isAfterBusinessDate(closedDate)) { - baseDataValidator.reset().parameter(SavingsApiConstants.closedOnDateParamName).value(closedDate) - .failWithCode("cannot.be.a.future.date"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - final List savingsAccountTransactions = retrieveListOfTransactions(); - 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); - } - } - } - - validateActivityNotBeforeClientOrGroupTransferDate(SavingsEvent.SAVINGS_CLOSE_ACCOUNT, closedDate); - this.status = SavingsAccountStatusType.PRE_MATURE_CLOSURE.getValue(); - - final Integer onAccountClosureId = command.integerValueOfParameterNamed(onAccountClosureIdParamName); - final DepositAccountOnClosureType onClosureType = DepositAccountOnClosureType.fromInt(onAccountClosureId); - this.accountTermAndPreClosure.updateOnAccountClosureStatus(onClosureType); - - /* - * // withdraw deposit amount before closing the account final Money transactionAmountMoney = - * Money.of(this.currency, this.getAccountBalance()); final SavingsAccountTransaction withdraw = - * SavingsAccountTransaction.withdrawal(this, office(), paymentDetail, closedDate, transactionAmountMoney, new - * Date()); this.transactions.add(withdraw); - */ - 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.format(fmt)); - - this.rejectedOnDate = null; - this.rejectedBy = null; - this.withdrawnOnDate = null; - this.withdrawnBy = null; - this.closedOnDate = closedDate; - this.closedBy = currentUser; - this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, this.transactions); - } - - @Override - public Money activateWithBalance() { - return Money.of(this.currency, this.accountTermAndPreClosure.depositAmount()); - } - - public void close(final AppUser currentUser, final JsonCommand command, final Map actualChanges) { - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(FIXED_DEPOSIT_ACCOUNT_RESOURCE_NAME + SavingsApiConstants.closeAction); - - final SavingsAccountStatusType currentStatus = SavingsAccountStatusType.fromInt(this.status); - if (!SavingsAccountStatusType.MATURED.hasStateOf(currentStatus)) { - baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("not.in.matured.state"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - final Locale locale = command.extractLocale(); - final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); - final LocalDate closedDate = command.localDateValueOfParameterNamed(SavingsApiConstants.closedOnDateParamName); - - if (DateUtils.isBefore(closedDate, getActivationDate())) { - baseDataValidator.reset().parameter(SavingsApiConstants.closedOnDateParamName).value(closedDate) - .failWithCode("must.be.after.activation.date"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - if (DateUtils.isBefore(closedDate, maturityDate())) { - baseDataValidator.reset().parameter(SavingsApiConstants.closedOnDateParamName).value(closedDate) - .failWithCode("must.be.after.account.maturity.date"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - if (DateUtils.isAfterBusinessDate(closedDate)) { - baseDataValidator.reset().parameter(SavingsApiConstants.closedOnDateParamName).value(closedDate) - .failWithCode("cannot.be.a.future.date"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - final List savingsAccountTransactions = retrieveListOfTransactions(); - 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); - } - } - } - - validateActivityNotBeforeClientOrGroupTransferDate(SavingsEvent.SAVINGS_CLOSE_ACCOUNT, closedDate); - this.status = SavingsAccountStatusType.CLOSED.getValue(); - - final Integer onAccountClosureId = command.integerValueOfParameterNamed(onAccountClosureIdParamName); - final DepositAccountOnClosureType onClosureType = DepositAccountOnClosureType.fromInt(onAccountClosureId); - this.accountTermAndPreClosure.updateOnAccountClosureStatus(onClosureType); - - // // withdraw deposit amount before closing the account - // final Money transactionAmountMoney = Money.of(this.currency, - // this.getAccountBalance()); - // final SavingsAccountTransaction withdraw = - // SavingsAccountTransaction.withdrawal(this, office(), paymentDetail, - // closedDate, - // transactionAmountMoney, new Date()); - // this.transactions.add(withdraw); - - 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.format(fmt)); - - this.rejectedOnDate = null; - this.rejectedBy = null; - this.withdrawnOnDate = null; - this.withdrawnBy = null; - this.closedOnDate = closedDate; - this.closedBy = currentUser; - // this.summary.updateSummary(this.currency, - // this.savingsAccountTransactionSummaryWrapper, this.transactions); - } - - public void updateClosedStatus() { - this.status = SavingsAccountStatusType.CLOSED.getValue(); - } - - public void updateOnAccountClosureStatus(DepositAccountOnClosureType onClosureType) { - this.accountTermAndPreClosure.updateOnAccountClosureStatus(onClosureType); - } - - public void postMaturityInterest(final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth) { - final LocalDate interestPostingUpToDate = maturityDate(); - final MathContext mc = MathContext.DECIMAL64; - final boolean isInterestTransfer = false; - final LocalDate postInterestOnDate = null; - final boolean backdatedTxnsAllowedTill = false; - boolean postReversals = false; - final List postingPeriods = calculateInterestUsing(mc, interestPostingUpToDate, isInterestTransfer, - isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, - postReversals); - - Money interestPostedToDate = Money.zero(this.currency); - - boolean recalucateDailyBalanceDetails = false; - - for (final PostingPeriod interestPostingPeriod : postingPeriods) { - - LocalDate interestPostingTransactionDate = interestPostingPeriod.dateOfPostingTransaction(); - - interestPostingTransactionDate = DateUtils.isAfter(interestPostingTransactionDate, interestPostingUpToDate) - ? interestPostingUpToDate - : interestPostingTransactionDate; - - final Money interestEarnedToBePostedForPeriod = interestPostingPeriod.getInterestEarned(); - - interestPostedToDate = interestPostedToDate.plus(interestEarnedToBePostedForPeriod); - - final SavingsAccountTransaction postingTransaction = findInterestPostingTransactionFor(interestPostingTransactionDate); - if (postingTransaction == null) { - final SavingsAccountTransaction newPostingTransaction = SavingsAccountTransaction.interestPosting(this, office(), - interestPostingTransactionDate, interestEarnedToBePostedForPeriod, interestPostingPeriod.isUserPosting()); - this.transactions.add(newPostingTransaction); - recalucateDailyBalanceDetails = true; - } else { - final boolean correctionRequired = postingTransaction.hasNotAmount(interestEarnedToBePostedForPeriod); - if (correctionRequired) { - postingTransaction.reverse(); - final SavingsAccountTransaction newPostingTransaction = SavingsAccountTransaction.interestPosting(this, office(), - interestPostingTransactionDate, interestEarnedToBePostedForPeriod, interestPostingPeriod.isUserPosting()); - this.transactions.add(newPostingTransaction); - recalucateDailyBalanceDetails = true; - } - } - } - recalucateDailyBalanceDetails = applyWithholdTaxForDepositAccounts(interestPostingUpToDate, recalucateDailyBalanceDetails, - backdatedTxnsAllowedTill); - if (recalucateDailyBalanceDetails) { - // update existing transactions so derived balance fields are - // correct. - recalculateDailyBalances(Money.zero(this.currency), interestPostingUpToDate, backdatedTxnsAllowedTill, postReversals); - } - - this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, this.transactions); - } - - public void postPreMaturityInterest(final LocalDate accountCloseDate, final boolean isPreMatureClosure, - final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth) { - - Money interestPostedToDate = totalInterestPosted(); - // calculate interest before one day of closure date - final LocalDate interestCalculatedToDate = accountCloseDate.minusDays(1); - final Money interestOnMaturity = calculatePreMatureInterest(interestCalculatedToDate, - retreiveOrderedNonInterestPostingTransactions(), isPreMatureClosure, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth); - boolean recalucateDailyBalance = false; - - // post remaining interest - final Money remainigInterestToBePosted = interestOnMaturity.minus(interestPostedToDate); - final boolean backdatedTxnsAllowedTill = false; - if (!remainigInterestToBePosted.isZero()) { - final boolean postInterestAsOn = false; - final SavingsAccountTransaction newPostingTransaction = SavingsAccountTransaction.interestPosting(this, office(), - accountCloseDate, remainigInterestToBePosted, postInterestAsOn); - this.transactions.add(newPostingTransaction); - recalucateDailyBalance = true; - } - - recalucateDailyBalance = applyWithholdTaxForDepositAccounts(accountCloseDate, recalucateDailyBalance, backdatedTxnsAllowedTill); - boolean postReversals = false; - if (recalucateDailyBalance) { - // update existing transactions so derived balance fields are - // correct. - recalculateDailyBalances(Money.zero(this.currency), accountCloseDate, backdatedTxnsAllowedTill, postReversals); - } - this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, this.transactions); - this.accountTermAndPreClosure.updateMaturityDetails(this.getAccountBalance(), accountCloseDate); - - } - - public BigDecimal calculatePreMatureAmount(final LocalDate preMatureDate, final boolean isPreMatureClosure, - final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth) { - - final Money interestPostedToDate = totalInterestPosted().copy(); - - final Money interestEarnedTillDate = calculatePreMatureInterest(preMatureDate, retreiveOrderedNonInterestPostingTransactions(), - isPreMatureClosure, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth); - - final Money accountBalance = Money.of(getCurrency(), getAccountBalance()); - final Money maturityAmount = accountBalance.minus(interestPostedToDate).plus(interestEarnedTillDate); - - return maturityAmount.getAmount(); - } - - private Money calculatePreMatureInterest(final LocalDate preMatureDate, final List transactions, - final boolean isPreMatureClosure, final boolean isSavingsInterestPostingAtCurrentPeriodEnd, - final Integer financialYearBeginningMonth) { - final MathContext mc = MathContext.DECIMAL64; - final List postingPeriods = calculateInterestPayable(mc, preMatureDate, transactions, isPreMatureClosure, - isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth); - - Money interestOnMaturity = Money.zero(this.currency); - - for (final PostingPeriod interestPostingPeriod : postingPeriods) { - final Money interestEarnedForPeriod = interestPostingPeriod.getInterestEarned(); - interestOnMaturity = interestOnMaturity.plus(interestEarnedForPeriod); - } - - return interestOnMaturity; - } - - public void postInterest(final MathContext mc, final LocalDate postingDate, boolean isInterestTransfer, - final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth, - final LocalDate postInterestOnDate, final boolean backdatedTxnsAllowedTill) { - final LocalDate interestPostingUpToDate = interestPostingUpToDate(postingDate); - boolean postReversals = false; - super.postInterest(mc, interestPostingUpToDate, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals); - } - - @Override - public List calculateInterestUsing(final MathContext mc, final LocalDate postingDate, boolean isInterestTransfer, - final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth, - final LocalDate postAsInterestOn, final boolean backdatedTxnsAllowedTill, final boolean postReversals) { - final LocalDate interestPostingUpToDate = interestPostingUpToDate(postingDate); - return super.calculateInterestUsing(mc, interestPostingUpToDate, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postAsInterestOn, backdatedTxnsAllowedTill, postReversals); - } - - private LocalDate interestPostingUpToDate(final LocalDate interestPostingDate) { - LocalDate interestPostingUpToDate = interestPostingDate; - final LocalDate uptoMaturityDate = interestCalculatedUpto(); - if (uptoMaturityDate != null && DateUtils.isBefore(uptoMaturityDate, interestPostingDate)) { - interestPostingUpToDate = uptoMaturityDate; - } - return interestPostingUpToDate; - } - - public LocalDate maturityDate() { - return this.accountTermAndPreClosure.getMaturityDate(); - } - - public BigDecimal maturityAmount() { - return this.accountTermAndPreClosure.maturityAmount(); - } - - private Money totalInterestPosted() { - Money interestPostedToDate = Money.zero(this.currency); - for (final SavingsAccountTransaction transaction : this.transactions) { - if (transaction.isInterestPostingAndNotReversed()) { - interestPostedToDate = interestPostedToDate.plus(transaction.getAmount(currency)); - } - } - - return interestPostedToDate; - } - - @Override - public Map activate(final AppUser currentUser, final JsonCommand command) { - final Map actualChanges = super.activate(currentUser, command); - - // if (isAccountLocked(calculateMaturityDate())) { - // final List dataValidationErrors = new - // ArrayList(); - // final DataValidatorBuilder baseDataValidator = new - // DataValidatorBuilder(dataValidationErrors) - // .resource(FIXED_DEPOSIT_ACCOUNT_RESOURCE_NAME); - // baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("deposit.period.must.be.greater.than.lock.in.period", - // "Deposit period must be greater than account lock-in period."); - // if (!dataValidationErrors.isEmpty()) { throw new - // PlatformApiDataValidationException(dataValidationErrors); } - // } - return actualChanges; - } - - private LocalDate depositStartDate() { - // TODO: Support to add deposit start date which can be a date after - // account activation date. - final LocalDate depositStartDate = accountSubmittedOrActivationDate(); - return depositStartDate; - } - - private LocalDate interestCalculatedUpto() { - LocalDate uptoMaturityDate = calculateMaturityDate(); - if (uptoMaturityDate != null) { - // interest should not be calculated for maturity day - uptoMaturityDate = uptoMaturityDate.minusDays(1); - } - return uptoMaturityDate; - } - - public void validateDomainRules() { - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(FIXED_DEPOSIT_ACCOUNT_RESOURCE_NAME); - validateDomainRules(baseDataValidator); - super.validateInterestPostingAndCompoundingPeriodTypes(baseDataValidator); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - private void validateDomainRules(final DataValidatorBuilder baseDataValidator) { - - final boolean isMinTermGreaterThanMax = this.accountTermAndPreClosure.depositTermDetail() - .isMinDepositTermGreaterThanMaxDepositTerm(); - final boolean isValidDepositPeriod = this.accountTermAndPreClosure.depositTermDetail().isDepositBetweenMinAndMax(depositStartDate(), - calculateMaturityDate()); - // deposit period should be within min and max deposit term - if (isMinTermGreaterThanMax) { - final Integer maxTerm = this.accountTermAndPreClosure.depositTermDetail().maxDepositTerm(); - baseDataValidator.reset().parameter(maxDepositTermParamName).value(maxTerm) - .failWithCodeNoParameterAddedToErrorCode("max.term.lessthan.min.term"); - } - final Integer depositPeriod = this.accountTermAndPreClosure.depositPeriod(); - final SavingsPeriodFrequencyType depositPeriodFrequencyType = this.accountTermAndPreClosure.depositPeriodFrequencyType(); - - if (!isValidDepositPeriod) { - baseDataValidator.reset().parameter(depositPeriodParamName).value(depositPeriod) - .failWithCodeNoParameterAddedToErrorCode("deposit.period.not.between.min.and.max.deposit.term"); - } else { - final Integer inMultiplesOf = this.accountTermAndPreClosure.depositTermDetail().inMultiplesOfDepositTerm(); - if (inMultiplesOf != null) { - final boolean isValid = this.accountTermAndPreClosure.depositTermDetail().isValidInMultiplesOfPeriod(depositPeriod, - depositPeriodFrequencyType); - if (!isValid) { - baseDataValidator.reset().parameter(depositPeriodParamName).value(depositPeriod) - .failWithCodeNoParameterAddedToErrorCode("deposit.period.not.multiple.of.term"); - } - } - } - - if (this.chart != null) { - final LocalDate chartFromDate = this.chart.getFromDate(); - LocalDate chartEndDate = this.chart.getEndDate(); - chartEndDate = chartEndDate == null ? DateUtils.getBusinessLocalDate() : chartEndDate; - - final LocalDateInterval chartInterval = LocalDateInterval.create(chartFromDate, chartEndDate); - if (!chartInterval.contains(accountSubmittedOrActivationDate())) { - baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("no.valid.interest.rate.slab.available"); - } - - final BigDecimal depositAmount = accountTermAndPreClosure.depositAmount(); - BigDecimal applicableInterestRate = this.chart.getApplicableInterestRate(depositAmount, depositStartDate(), - calculateMaturityDate(), this.client); - - if (applicableInterestRate.compareTo(BigDecimal.ZERO) == 0) { - baseDataValidator.reset() - .failWithCodeNoParameterAddedToErrorCode("no.applicable.interest.rate.is.found.based.on.amount.and.deposit.period"); - } - - } else if (this.nominalAnnualInterestRate == null || this.nominalAnnualInterestRate.compareTo(BigDecimal.ZERO) == 0) { - baseDataValidator.reset().parameter(DepositsApiConstants.nominalAnnualInterestRateParamName).value(nominalAnnualInterestRate) - .failWithCodeNoParameterAddedToErrorCode("valid.interest.chart.or.nominal.interest.rate.required"); - } - - } - - public boolean isReinvestOnClosure() { - return this.accountTermAndPreClosure.isReinvestOnClosure(); - } - - public boolean isTransferToSavingsOnClosure() { - return this.accountTermAndPreClosure.isTransferToSavingsOnClosure(); - } - - public Integer getOnAccountClosureId() { - return this.accountTermAndPreClosure.getOnAccountClosureType(); - } - - public Long getTransferToSavingsAccountId() { - return this.accountTermAndPreClosure.getTransferToSavingsAccountId(); - } - - public FixedDepositAccount reInvest(BigDecimal depositAmount, ExternalId externalId) { - - final DepositAccountTermAndPreClosure newAccountTermAndPreClosure = this.accountTermAndPreClosure.copy(depositAmount); - final SavingsProduct product = this.product; - final InterestRateChart productChart = product.applicableChart(getClosedOnDate()); - final DepositAccountInterestRateChart newChart = DepositAccountInterestRateChart.from(productChart); - - final AccountType accountType = AccountType.fromInt(this.accountType); - final SavingsPostingInterestPeriodType postingPeriodType = SavingsPostingInterestPeriodType.fromInt(this.interestPostingPeriodType); - final SavingsCompoundingInterestPeriodType compoundingPeriodType = SavingsCompoundingInterestPeriodType - .fromInt(this.interestCompoundingPeriodType); - final SavingsInterestCalculationType interestCalculationType = SavingsInterestCalculationType.fromInt(this.interestCalculationType); - final SavingsInterestCalculationDaysInYearType daysInYearType = SavingsInterestCalculationDaysInYearType - .fromInt(this.interestCalculationDaysInYearType); - final BigDecimal minRequiredOpeningBalance = null; - final BigDecimal interestRate = BigDecimal.ZERO; - final Set savingsAccountCharges = null; - final SavingsPeriodFrequencyType lockinPeriodFrequencyType = SavingsPeriodFrequencyType.fromInt(this.lockinPeriodFrequencyType); - final Integer lockinPeriodFrequency = this.lockinPeriodFrequency; - final boolean withdrawalFeeApplicableForTransfer = false; - final String accountNumber = null; - final boolean withHoldTax = this.withHoldTax; - final FixedDepositAccount reInvestedAccount = FixedDepositAccount.createNewApplicationForSubmittal(client, group, product, - savingsOfficer, accountNumber, externalId, accountType, getClosedOnDate(), closedBy, interestRate, compoundingPeriodType, - postingPeriodType, interestCalculationType, daysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency, - lockinPeriodFrequencyType, withdrawalFeeApplicableForTransfer, savingsAccountCharges, newAccountTermAndPreClosure, newChart, - withHoldTax); - - newAccountTermAndPreClosure.updateAccountReference(reInvestedAccount); - newChart.updateDepositAccountReference(reInvestedAccount); - - return reInvestedAccount; - - } - - @Override - protected boolean isTransferInterestToOtherAccount() { - return this.accountTermAndPreClosure.isTransferInterestToLinkedAccount(); - } - - @Override - public boolean allowDeposit() { - return false; - } - - @Override - public boolean allowWithdrawal() { - return false; - } - - @Override - public boolean allowModify() { - return false; - } - - @Override - public boolean isTransactionsAllowed() { - return isActive() || isAccountMatured(); - } - - private boolean isAccountMatured() { - return SavingsAccountStatusType.fromInt(status).isMatured(); - } - - @Override - public BigDecimal minBalanceForInterestCalculation() { - return null; - } - - @Override - public void loadLazyCollections() { - super.loadLazyCollections(); - this.chart.getId(); - } - - public BigDecimal getDepositAmount() { - return this.accountTermAndPreClosure.depositAmount(); - } - - @Override - public BigDecimal getAccountBalance() { - return this.summary.getAccountBalance(this.currency).getAmount(); - } - - public boolean isMatured() { - return SavingsAccountStatusType.MATURED.getValue().equals(this.status); - } - - public void setClosedOnDate(final LocalDate closedOnDate) { - this.closedOnDate = closedOnDate; - } - - @Override - public DepositAccountType depositAccountType() { - return DepositAccountType.fromInt(200); - } -} +/** + * 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.DepositsApiConstants.FIXED_DEPOSIT_ACCOUNT_RESOURCE_NAME; +import static org.apache.fineract.portfolio.savings.DepositsApiConstants.depositPeriodParamName; +import static org.apache.fineract.portfolio.savings.DepositsApiConstants.maxDepositTermParamName; +import static org.apache.fineract.portfolio.savings.DepositsApiConstants.onAccountClosureIdParamName; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Transient; +import java.math.BigDecimal; +import java.math.MathContext; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; +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.ExternalId; +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.organisation.monetary.domain.Money; +import org.apache.fineract.organisation.staff.domain.Staff; +import org.apache.fineract.portfolio.accountdetails.domain.AccountType; +import org.apache.fineract.portfolio.client.domain.Client; +import org.apache.fineract.portfolio.group.domain.Group; +import org.apache.fineract.portfolio.interestratechart.domain.InterestRateChart; +import org.apache.fineract.portfolio.interestratechart.service.InterestRateChartAssembler; +import org.apache.fineract.portfolio.savings.DepositAccountOnClosureType; +import org.apache.fineract.portfolio.savings.DepositAccountType; +import org.apache.fineract.portfolio.savings.DepositsApiConstants; +import org.apache.fineract.portfolio.savings.PreClosurePenalInterestOnType; +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.domain.interest.PostingPeriod; +import org.apache.fineract.portfolio.savings.domain.interest.SavingsAccountTransactionDetailsForPostingPeriod; +import org.apache.fineract.portfolio.savings.service.SavingsEnumerations; +import org.apache.fineract.useradministration.domain.AppUser; + +@Entity +@DiscriminatorValue("200") +public class FixedDepositAccount extends SavingsAccount { + + @OneToOne(mappedBy = "account", cascade = CascadeType.ALL) + private DepositAccountTermAndPreClosure accountTermAndPreClosure; + + @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "account") + protected DepositAccountInterestRateChart chart; + + @Transient + protected InterestRateChartAssembler chartAssembler; + @Transient + private ConfigurationDomainService configurationDomainService; + + protected FixedDepositAccount() { + // + } + + public static FixedDepositAccount createNewApplicationForSubmittal(final Client client, final Group group, final SavingsProduct product, + final Staff fieldOfficer, final String accountNo, final ExternalId 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 savingsAccountCharges, + final DepositAccountTermAndPreClosure accountTermAndPreClosure, final DepositAccountInterestRateChart chart, + boolean withHoldTax) { + + final SavingsAccountStatusType status = SavingsAccountStatusType.SUBMITTED_AND_PENDING_APPROVAL; + final boolean allowOverdraft = false; + final BigDecimal overdraftLimit = new BigDecimal(0); + FixedDepositAccount account = new FixedDepositAccount(client, group, product, fieldOfficer, accountNo, externalId, status, + accountType, submittedOnDate, submittedBy, interestRate, interestCompoundingPeriodType, interestPostingPeriodType, + interestCalculationType, interestCalculationDaysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency, + lockinPeriodFrequencyType, withdrawalFeeApplicableForTransfer, savingsAccountCharges, accountTermAndPreClosure, chart, + allowOverdraft, overdraftLimit, withHoldTax); + + return account; + } + + private FixedDepositAccount(final Client client, final Group group, final SavingsProduct product, final Staff fieldOfficer, + final String accountNo, final ExternalId 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 savingsAccountCharges, + final DepositAccountTermAndPreClosure accountTermAndPreClosure, DepositAccountInterestRateChart chart, + final boolean allowOverdraft, final BigDecimal overdraftLimit, boolean withHoldTax) { + + super(client, group, product, fieldOfficer, accountNo, externalId, status, accountType, submittedOnDate, submittedBy, + nominalAnnualInterestRate, interestCompoundingPeriodType, interestPostingPeriodType, interestCalculationType, + interestCalculationDaysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType, + withdrawalFeeApplicableForTransfer, savingsAccountCharges, allowOverdraft, overdraftLimit, withHoldTax); + + this.accountTermAndPreClosure = accountTermAndPreClosure; + this.chart = chart; + if (this.chart != null) { + this.chart.updateDepositAccountReference(this); + } + } + + @Override + public void modifyApplication(final JsonCommand command, final Map actualChanges) { + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(FIXED_DEPOSIT_ACCOUNT_RESOURCE_NAME + SavingsApiConstants.modifyApplicationAction); + super.modifyApplication(command, actualChanges, baseDataValidator); + final Map termAndPreClosureChanges = accountTermAndPreClosure.update(command, baseDataValidator); + actualChanges.putAll(termAndPreClosureChanges); + validateDomainRules(baseDataValidator); + super.validateInterestPostingAndCompoundingPeriodTypes(baseDataValidator); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + @Override + protected BigDecimal getEffectiveInterestRateAsFraction(final MathContext mc, final LocalDate interestPostingUpToDate) { + boolean isPreMatureClosure = false; + return getEffectiveInterestRateAsFraction(mc, interestPostingUpToDate, isPreMatureClosure); + } + + protected BigDecimal getEffectiveInterestRateAsFraction(final MathContext mc, final LocalDate interestPostingUpToDate, + final boolean isPreMatureClosure) { + + // default it to nominalAnnualInterst rate. interest chart overrrides + // this value. + BigDecimal applicableInterestRate = this.nominalAnnualInterestRate; + if (this.chart != null) { + boolean applyPreMaturePenalty = false; + BigDecimal penalInterest = BigDecimal.ZERO; + LocalDate depositCloseDate = calculateMaturityDate(); + if (isPreMatureClosure) { + if (this.accountTermAndPreClosure.isPreClosurePenalApplicable()) { + applyPreMaturePenalty = true; + penalInterest = this.accountTermAndPreClosure.depositPreClosureDetail().preClosurePenalInterest(); + final PreClosurePenalInterestOnType preClosurePenalInterestOnType = this.accountTermAndPreClosure + .depositPreClosureDetail().preClosurePenalInterestOnType(); + if (preClosurePenalInterestOnType.isWholeTerm()) { + depositCloseDate = interestCalculatedUpto(); + } else if (preClosurePenalInterestOnType.isTillPrematureWithdrawal()) { + depositCloseDate = interestPostingUpToDate; + } + } + } + + final BigDecimal depositAmount = accountTermAndPreClosure.depositAmount(); + applicableInterestRate = this.chart.getApplicableInterestRate(depositAmount, depositStartDate(), depositCloseDate, this.client); + + // CBS: override with senior citizen rate if applicable + if (isSeniorCitizen() && getSeniorCitizenInterestRate() != null + && getSeniorCitizenInterestRate().compareTo(BigDecimal.ZERO) > 0) { + applicableInterestRate = getSeniorCitizenInterestRate(); + } + + if (applyPreMaturePenalty) { + applicableInterestRate = applicableInterestRate.subtract(penalInterest); + applicableInterestRate = applicableInterestRate.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : applicableInterestRate; + } + } + this.nominalAnnualInterestRate = applicableInterestRate; + + return applicableInterestRate.divide(BigDecimal.valueOf(100L), mc); + } + + @Override + public BigDecimal getEffectiveInterestRateAsFractionAccrual(final MathContext mc, final LocalDate upToInterestCalculationDate) { + // CBS: accrual must also use senior citizen rate to stay consistent with maturity calculation + if (isSeniorCitizen() && getSeniorCitizenInterestRate() != null + && getSeniorCitizenInterestRate().compareTo(BigDecimal.ZERO) > 0) { + return getSeniorCitizenInterestRate().divide(BigDecimal.valueOf(100L), mc); + } + return super.getEffectiveInterestRateAsFractionAccrual(mc, upToInterestCalculationDate); + } + + public void updateMaturityDateAndAmountBeforeAccountActivation(final MathContext mc, final boolean isPreMatureClosure, + final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth) { + List allTransactions = new ArrayList<>(); + String refNo = null; + final Money transactionAmountMoney = Money.of(getCurrency(), this.accountTermAndPreClosure.depositAmount()); + final SavingsAccountTransaction transaction = SavingsAccountTransaction.deposit(null, office(), null, + this.accountSubmittedOrActivationDate(), transactionAmountMoney, refNo); + transaction.setRunningBalance(transactionAmountMoney); + transaction.updateCumulativeBalanceAndDates(this.getCurrency(), interestCalculatedUpto()); + allTransactions.add(transaction); + updateMaturityDateAndAmount(mc, allTransactions, isPreMatureClosure, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth); + } + + public void updateMaturityDateAndAmount(final MathContext mc, final boolean isPreMatureClosure, + final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth) { + updateMaturityDateAndAmount(mc, retreiveOrderedNonInterestPostingTransactions(), isPreMatureClosure, + isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth); + } + + public void updateMaturityDateAndAmount(final MathContext mc, final List transactions, + final boolean isPreMatureClosure, final boolean isSavingsInterestPostingAtCurrentPeriodEnd, + final Integer financialYearBeginningMonth) { + final LocalDate maturityDate = calculateMaturityDate(); + final LocalDate interestCalculationUpto = maturityDate.minusDays(1); + + // set end of day balance to maturity date for maturity interest + // calculation + this.resetAccountTransactionsEndOfDayBalances(transactions, maturityDate); + + final List postingPeriods = calculateInterestPayable(mc, interestCalculationUpto, transactions, isPreMatureClosure, + isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth); + + // reset end of day balance back to today's date + this.resetAccountTransactionsEndOfDayBalances(transactions, DateUtils.getBusinessLocalDate()); + + Money totalInterestPayable = Money.zero(getCurrency()); + for (PostingPeriod postingPeriod : postingPeriods) { + totalInterestPayable = totalInterestPayable.plus(postingPeriod.getInterestEarned()); + } + final Money depositAmount = Money.of(getCurrency(), this.accountTermAndPreClosure.depositAmount()); + final Money maturityAmount = depositAmount.plus(totalInterestPayable); + + this.accountTermAndPreClosure.updateMaturityDetails(maturityAmount.getAmount(), maturityDate); + } + + public void updateMaturityStatus(final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth) { + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(FIXED_DEPOSIT_ACCOUNT_RESOURCE_NAME + SavingsApiConstants.updateMaturityDetailsAction); + + 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); + } + } + + if (!DateUtils.isDateInTheFuture(maturityDate())) { + // update account status + this.status = SavingsAccountStatusType.MATURED.getValue(); + postMaturityInterest(isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth); + } + } + + public LocalDate calculateMaturityDate() { + + final LocalDate startDate = accountSubmittedOrActivationDate(); + LocalDate maturityDate = null; + final Integer depositPeriod = this.accountTermAndPreClosure.depositPeriod(); + switch (this.accountTermAndPreClosure.depositPeriodFrequencyType()) { + case DAYS: + maturityDate = startDate.plusDays(depositPeriod); + break; + case WEEKS: + maturityDate = startDate.plusWeeks(depositPeriod); + break; + case MONTHS: + maturityDate = startDate.plusMonths(depositPeriod); + break; + case YEARS: + maturityDate = startDate.plusYears(depositPeriod); + break; + case INVALID: + break; + } + + return maturityDate; + } + + private List calculateInterestPayable(final MathContext mc, final LocalDate maturityDate, + final List transactions, final boolean isPreMatureClosure, + final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth) { + + final SavingsPostingInterestPeriodType postingPeriodType = SavingsPostingInterestPeriodType.fromInt(this.interestPostingPeriodType); + + final SavingsCompoundingInterestPeriodType compoundingPeriodType = SavingsCompoundingInterestPeriodType + .fromInt(this.interestCompoundingPeriodType); + + final SavingsInterestCalculationDaysInYearType daysInYearType = SavingsInterestCalculationDaysInYearType + .fromInt(this.interestCalculationDaysInYearType); + List postedAsOnTransactionDates = getManualPostingDates(); + + final List postingPeriodIntervals = this.savingsHelper.determineInterestPostingPeriods( + accountSubmittedOrActivationDate(), maturityDate, postingPeriodType, financialYearBeginningMonth, + postedAsOnTransactionDates); + + final List allPostingPeriods = new ArrayList<>(); + + Money periodStartingBalance = Money.zero(currency); + + final SavingsInterestCalculationType interestCalculationType = SavingsInterestCalculationType.fromInt(this.interestCalculationType); + final BigDecimal interestRateAsFraction = getEffectiveInterestRateAsFraction(mc, maturityDate, isPreMatureClosure); + final Collection interestPostTransactions = this.savingsHelper.fetchPostInterestTransactionIds(getId()); + boolean isInterestTransfer = false; + final Money minBalanceForInterestCalculation = Money.of(getCurrency(), minBalanceForInterestCalculation()); + List savingsAccountTransactionDetailsForPostingPeriodList = toSavingsAccountTransactionDetailsForPostingPeriodList( + transactions); + for (final LocalDateInterval periodInterval : postingPeriodIntervals) { + boolean isUserPosting = false; + if (postedAsOnTransactionDates.contains(periodInterval.endDate())) { + isUserPosting = true; + } + final PostingPeriod postingPeriod = PostingPeriod.createFrom(periodInterval, periodStartingBalance, + savingsAccountTransactionDetailsForPostingPeriodList, this.currency, compoundingPeriodType, interestCalculationType, + interestRateAsFraction, daysInYearType.getValue(), maturityDate, interestPostTransactions, isInterestTransfer, + minBalanceForInterestCalculation, isSavingsInterestPostingAtCurrentPeriodEnd, isUserPosting, + financialYearBeginningMonth); + + periodStartingBalance = postingPeriod.closingBalance(); + + allPostingPeriods.add(postingPeriod); + } + + this.summary.updateFromInterestPeriodSummaries(this.currency, allPostingPeriods); + this.savingsHelper.calculateInterestForAllPostingPeriods(this.currency, allPostingPeriods, getLockedInUntilDate(), + isTransferInterestToOtherAccount()); + return allPostingPeriods; + } + + public void prematureClosure(final AppUser currentUser, final JsonCommand command, final Map actualChanges) { + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(FIXED_DEPOSIT_ACCOUNT_RESOURCE_NAME + DepositsApiConstants.preMatureCloseAction); + + 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 = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); + final LocalDate closedDate = command.localDateValueOfParameterNamed(SavingsApiConstants.closedOnDateParamName); + + if (DateUtils.isBefore(closedDate, getActivationDate())) { + baseDataValidator.reset().parameter(SavingsApiConstants.closedOnDateParamName).value(closedDate) + .failWithCode("must.be.after.activation.date"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + if (isAccountLocked(closedDate)) { + baseDataValidator.reset().parameter(SavingsApiConstants.closedOnDateParamName).value(closedDate) + .failWithCode("must.be.after.lockin.period"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + if (maturityDate() != null && DateUtils.isAfter(closedDate, maturityDate())) { + baseDataValidator.reset().parameter(SavingsApiConstants.closedOnDateParamName).value(closedDate) + .failWithCode("must.be.before.maturity.date"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + if (DateUtils.isAfterBusinessDate(closedDate)) { + baseDataValidator.reset().parameter(SavingsApiConstants.closedOnDateParamName).value(closedDate) + .failWithCode("cannot.be.a.future.date"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + final List savingsAccountTransactions = retrieveListOfTransactions(); + 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); + } + } + } + + validateActivityNotBeforeClientOrGroupTransferDate(SavingsEvent.SAVINGS_CLOSE_ACCOUNT, closedDate); + this.status = SavingsAccountStatusType.PRE_MATURE_CLOSURE.getValue(); + + final Integer onAccountClosureId = command.integerValueOfParameterNamed(onAccountClosureIdParamName); + final DepositAccountOnClosureType onClosureType = DepositAccountOnClosureType.fromInt(onAccountClosureId); + this.accountTermAndPreClosure.updateOnAccountClosureStatus(onClosureType); + + /* + * // withdraw deposit amount before closing the account final Money transactionAmountMoney = + * Money.of(this.currency, this.getAccountBalance()); final SavingsAccountTransaction withdraw = + * SavingsAccountTransaction.withdrawal(this, office(), paymentDetail, closedDate, transactionAmountMoney, new + * Date()); this.transactions.add(withdraw); + */ + 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.format(fmt)); + + this.rejectedOnDate = null; + this.rejectedBy = null; + this.withdrawnOnDate = null; + this.withdrawnBy = null; + this.closedOnDate = closedDate; + this.closedBy = currentUser; + this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, this.transactions); + } + + @Override + public Money activateWithBalance() { + return Money.of(this.currency, this.accountTermAndPreClosure.depositAmount()); + } + + public void close(final AppUser currentUser, final JsonCommand command, final Map actualChanges) { + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(FIXED_DEPOSIT_ACCOUNT_RESOURCE_NAME + SavingsApiConstants.closeAction); + + final SavingsAccountStatusType currentStatus = SavingsAccountStatusType.fromInt(this.status); + if (!SavingsAccountStatusType.MATURED.hasStateOf(currentStatus)) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("not.in.matured.state"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + final Locale locale = command.extractLocale(); + final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); + final LocalDate closedDate = command.localDateValueOfParameterNamed(SavingsApiConstants.closedOnDateParamName); + + if (DateUtils.isBefore(closedDate, getActivationDate())) { + baseDataValidator.reset().parameter(SavingsApiConstants.closedOnDateParamName).value(closedDate) + .failWithCode("must.be.after.activation.date"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + if (DateUtils.isBefore(closedDate, maturityDate())) { + baseDataValidator.reset().parameter(SavingsApiConstants.closedOnDateParamName).value(closedDate) + .failWithCode("must.be.after.account.maturity.date"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + if (DateUtils.isAfterBusinessDate(closedDate)) { + baseDataValidator.reset().parameter(SavingsApiConstants.closedOnDateParamName).value(closedDate) + .failWithCode("cannot.be.a.future.date"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + final List savingsAccountTransactions = retrieveListOfTransactions(); + 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); + } + } + } + + validateActivityNotBeforeClientOrGroupTransferDate(SavingsEvent.SAVINGS_CLOSE_ACCOUNT, closedDate); + this.status = SavingsAccountStatusType.CLOSED.getValue(); + + final Integer onAccountClosureId = command.integerValueOfParameterNamed(onAccountClosureIdParamName); + final DepositAccountOnClosureType onClosureType = DepositAccountOnClosureType.fromInt(onAccountClosureId); + this.accountTermAndPreClosure.updateOnAccountClosureStatus(onClosureType); + + // // withdraw deposit amount before closing the account + // final Money transactionAmountMoney = Money.of(this.currency, + // this.getAccountBalance()); + // final SavingsAccountTransaction withdraw = + // SavingsAccountTransaction.withdrawal(this, office(), paymentDetail, + // closedDate, + // transactionAmountMoney, new Date()); + // this.transactions.add(withdraw); + + 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.format(fmt)); + + this.rejectedOnDate = null; + this.rejectedBy = null; + this.withdrawnOnDate = null; + this.withdrawnBy = null; + this.closedOnDate = closedDate; + this.closedBy = currentUser; + // this.summary.updateSummary(this.currency, + // this.savingsAccountTransactionSummaryWrapper, this.transactions); + } + + public void updateClosedStatus() { + this.status = SavingsAccountStatusType.CLOSED.getValue(); + } + + public void updateOnAccountClosureStatus(DepositAccountOnClosureType onClosureType) { + this.accountTermAndPreClosure.updateOnAccountClosureStatus(onClosureType); + } + + public void postMaturityInterest(final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth) { + final LocalDate interestPostingUpToDate = maturityDate(); + final MathContext mc = MathContext.DECIMAL64; + final boolean isInterestTransfer = false; + final LocalDate postInterestOnDate = null; + final boolean backdatedTxnsAllowedTill = false; + boolean postReversals = false; + final List postingPeriods = calculateInterestUsing(mc, interestPostingUpToDate, isInterestTransfer, + isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, + postReversals); + + Money interestPostedToDate = Money.zero(this.currency); + + boolean recalucateDailyBalanceDetails = false; + + for (final PostingPeriod interestPostingPeriod : postingPeriods) { + + LocalDate interestPostingTransactionDate = interestPostingPeriod.dateOfPostingTransaction(); + + interestPostingTransactionDate = DateUtils.isAfter(interestPostingTransactionDate, interestPostingUpToDate) + ? interestPostingUpToDate + : interestPostingTransactionDate; + + final Money interestEarnedToBePostedForPeriod = interestPostingPeriod.getInterestEarned(); + + interestPostedToDate = interestPostedToDate.plus(interestEarnedToBePostedForPeriod); + + final SavingsAccountTransaction postingTransaction = findInterestPostingTransactionFor(interestPostingTransactionDate); + if (postingTransaction == null) { + final SavingsAccountTransaction newPostingTransaction = SavingsAccountTransaction.interestPosting(this, office(), + interestPostingTransactionDate, interestEarnedToBePostedForPeriod, interestPostingPeriod.isUserPosting()); + this.transactions.add(newPostingTransaction); + recalucateDailyBalanceDetails = true; + } else { + final boolean correctionRequired = postingTransaction.hasNotAmount(interestEarnedToBePostedForPeriod); + if (correctionRequired) { + postingTransaction.reverse(); + final SavingsAccountTransaction newPostingTransaction = SavingsAccountTransaction.interestPosting(this, office(), + interestPostingTransactionDate, interestEarnedToBePostedForPeriod, interestPostingPeriod.isUserPosting()); + this.transactions.add(newPostingTransaction); + recalucateDailyBalanceDetails = true; + } + } + } + recalucateDailyBalanceDetails = applyWithholdTaxForDepositAccounts(interestPostingUpToDate, recalucateDailyBalanceDetails, + backdatedTxnsAllowedTill); + if (recalucateDailyBalanceDetails) { + // update existing transactions so derived balance fields are + // correct. + recalculateDailyBalances(Money.zero(this.currency), interestPostingUpToDate, backdatedTxnsAllowedTill, postReversals); + } + + this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, this.transactions); + } + + public void postPreMaturityInterest(final LocalDate accountCloseDate, final boolean isPreMatureClosure, + final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth) { + + Money interestPostedToDate = totalInterestPosted(); + // calculate interest before one day of closure date + final LocalDate interestCalculatedToDate = accountCloseDate.minusDays(1); + final Money interestOnMaturity = calculatePreMatureInterest(interestCalculatedToDate, + retreiveOrderedNonInterestPostingTransactions(), isPreMatureClosure, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth); + boolean recalucateDailyBalance = false; + + // post remaining interest + final Money remainigInterestToBePosted = interestOnMaturity.minus(interestPostedToDate); + final boolean backdatedTxnsAllowedTill = false; + if (!remainigInterestToBePosted.isZero()) { + final boolean postInterestAsOn = false; + final SavingsAccountTransaction newPostingTransaction = SavingsAccountTransaction.interestPosting(this, office(), + accountCloseDate, remainigInterestToBePosted, postInterestAsOn); + this.transactions.add(newPostingTransaction); + recalucateDailyBalance = true; + } + + recalucateDailyBalance = applyWithholdTaxForDepositAccounts(accountCloseDate, recalucateDailyBalance, backdatedTxnsAllowedTill); + boolean postReversals = false; + if (recalucateDailyBalance) { + // update existing transactions so derived balance fields are + // correct. + recalculateDailyBalances(Money.zero(this.currency), accountCloseDate, backdatedTxnsAllowedTill, postReversals); + } + this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, this.transactions); + this.accountTermAndPreClosure.updateMaturityDetails(this.getAccountBalance(), accountCloseDate); + + } + + public BigDecimal calculatePreMatureAmount(final LocalDate preMatureDate, final boolean isPreMatureClosure, + final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth) { + + final Money interestPostedToDate = totalInterestPosted().copy(); + + final Money interestEarnedTillDate = calculatePreMatureInterest(preMatureDate, retreiveOrderedNonInterestPostingTransactions(), + isPreMatureClosure, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth); + + final Money accountBalance = Money.of(getCurrency(), getAccountBalance()); + final Money maturityAmount = accountBalance.minus(interestPostedToDate).plus(interestEarnedTillDate); + + return maturityAmount.getAmount(); + } + + private Money calculatePreMatureInterest(final LocalDate preMatureDate, final List transactions, + final boolean isPreMatureClosure, final boolean isSavingsInterestPostingAtCurrentPeriodEnd, + final Integer financialYearBeginningMonth) { + final MathContext mc = MathContext.DECIMAL64; + final List postingPeriods = calculateInterestPayable(mc, preMatureDate, transactions, isPreMatureClosure, + isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth); + + Money interestOnMaturity = Money.zero(this.currency); + + for (final PostingPeriod interestPostingPeriod : postingPeriods) { + final Money interestEarnedForPeriod = interestPostingPeriod.getInterestEarned(); + interestOnMaturity = interestOnMaturity.plus(interestEarnedForPeriod); + } + + return interestOnMaturity; + } + + public void postInterest(final MathContext mc, final LocalDate postingDate, boolean isInterestTransfer, + final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth, + final LocalDate postInterestOnDate, final boolean backdatedTxnsAllowedTill) { + final LocalDate interestPostingUpToDate = interestPostingUpToDate(postingDate); + boolean postReversals = false; + super.postInterest(mc, interestPostingUpToDate, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals); + } + + @Override + public List calculateInterestUsing(final MathContext mc, final LocalDate postingDate, boolean isInterestTransfer, + final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth, + final LocalDate postAsInterestOn, final boolean backdatedTxnsAllowedTill, final boolean postReversals) { + final LocalDate interestPostingUpToDate = interestPostingUpToDate(postingDate); + return super.calculateInterestUsing(mc, interestPostingUpToDate, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth, postAsInterestOn, backdatedTxnsAllowedTill, postReversals); + } + + private LocalDate interestPostingUpToDate(final LocalDate interestPostingDate) { + LocalDate interestPostingUpToDate = interestPostingDate; + final LocalDate uptoMaturityDate = interestCalculatedUpto(); + if (uptoMaturityDate != null && DateUtils.isBefore(uptoMaturityDate, interestPostingDate)) { + interestPostingUpToDate = uptoMaturityDate; + } + return interestPostingUpToDate; + } + + public LocalDate maturityDate() { + return this.accountTermAndPreClosure.getMaturityDate(); + } + + public BigDecimal maturityAmount() { + return this.accountTermAndPreClosure.maturityAmount(); + } + + private Money totalInterestPosted() { + Money interestPostedToDate = Money.zero(this.currency); + for (final SavingsAccountTransaction transaction : this.transactions) { + if (transaction.isInterestPostingAndNotReversed()) { + interestPostedToDate = interestPostedToDate.plus(transaction.getAmount(currency)); + } + } + + return interestPostedToDate; + } + + @Override + public Map activate(final AppUser currentUser, final JsonCommand command) { + final Map actualChanges = super.activate(currentUser, command); + + // if (isAccountLocked(calculateMaturityDate())) { + // final List dataValidationErrors = new + // ArrayList(); + // final DataValidatorBuilder baseDataValidator = new + // DataValidatorBuilder(dataValidationErrors) + // .resource(FIXED_DEPOSIT_ACCOUNT_RESOURCE_NAME); + // baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("deposit.period.must.be.greater.than.lock.in.period", + // "Deposit period must be greater than account lock-in period."); + // if (!dataValidationErrors.isEmpty()) { throw new + // PlatformApiDataValidationException(dataValidationErrors); } + // } + return actualChanges; + } + + private LocalDate depositStartDate() { + // TODO: Support to add deposit start date which can be a date after + // account activation date. + final LocalDate depositStartDate = accountSubmittedOrActivationDate(); + return depositStartDate; + } + + private LocalDate interestCalculatedUpto() { + LocalDate uptoMaturityDate = calculateMaturityDate(); + if (uptoMaturityDate != null) { + // interest should not be calculated for maturity day + uptoMaturityDate = uptoMaturityDate.minusDays(1); + } + return uptoMaturityDate; + } + + public void validateDomainRules() { + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(FIXED_DEPOSIT_ACCOUNT_RESOURCE_NAME); + validateDomainRules(baseDataValidator); + super.validateInterestPostingAndCompoundingPeriodTypes(baseDataValidator); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + private void validateDomainRules(final DataValidatorBuilder baseDataValidator) { + + final boolean isMinTermGreaterThanMax = this.accountTermAndPreClosure.depositTermDetail() + .isMinDepositTermGreaterThanMaxDepositTerm(); + final boolean isValidDepositPeriod = this.accountTermAndPreClosure.depositTermDetail().isDepositBetweenMinAndMax(depositStartDate(), + calculateMaturityDate()); + // deposit period should be within min and max deposit term + if (isMinTermGreaterThanMax) { + final Integer maxTerm = this.accountTermAndPreClosure.depositTermDetail().maxDepositTerm(); + baseDataValidator.reset().parameter(maxDepositTermParamName).value(maxTerm) + .failWithCodeNoParameterAddedToErrorCode("max.term.lessthan.min.term"); + } + final Integer depositPeriod = this.accountTermAndPreClosure.depositPeriod(); + final SavingsPeriodFrequencyType depositPeriodFrequencyType = this.accountTermAndPreClosure.depositPeriodFrequencyType(); + + if (!isValidDepositPeriod) { + baseDataValidator.reset().parameter(depositPeriodParamName).value(depositPeriod) + .failWithCodeNoParameterAddedToErrorCode("deposit.period.not.between.min.and.max.deposit.term"); + } else { + final Integer inMultiplesOf = this.accountTermAndPreClosure.depositTermDetail().inMultiplesOfDepositTerm(); + if (inMultiplesOf != null) { + final boolean isValid = this.accountTermAndPreClosure.depositTermDetail().isValidInMultiplesOfPeriod(depositPeriod, + depositPeriodFrequencyType); + if (!isValid) { + baseDataValidator.reset().parameter(depositPeriodParamName).value(depositPeriod) + .failWithCodeNoParameterAddedToErrorCode("deposit.period.not.multiple.of.term"); + } + } + } + + if (this.chart != null) { + final LocalDate chartFromDate = this.chart.getFromDate(); + LocalDate chartEndDate = this.chart.getEndDate(); + chartEndDate = chartEndDate == null ? DateUtils.getBusinessLocalDate() : chartEndDate; + + final LocalDateInterval chartInterval = LocalDateInterval.create(chartFromDate, chartEndDate); + if (!chartInterval.contains(accountSubmittedOrActivationDate())) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("no.valid.interest.rate.slab.available"); + } + + final BigDecimal depositAmount = accountTermAndPreClosure.depositAmount(); + BigDecimal applicableInterestRate = this.chart.getApplicableInterestRate(depositAmount, depositStartDate(), + calculateMaturityDate(), this.client); + + if (applicableInterestRate.compareTo(BigDecimal.ZERO) == 0) { + baseDataValidator.reset() + .failWithCodeNoParameterAddedToErrorCode("no.applicable.interest.rate.is.found.based.on.amount.and.deposit.period"); + } + + } else if (this.nominalAnnualInterestRate == null || this.nominalAnnualInterestRate.compareTo(BigDecimal.ZERO) == 0) { + baseDataValidator.reset().parameter(DepositsApiConstants.nominalAnnualInterestRateParamName).value(nominalAnnualInterestRate) + .failWithCodeNoParameterAddedToErrorCode("valid.interest.chart.or.nominal.interest.rate.required"); + } + + } + + public boolean isReinvestOnClosure() { + return this.accountTermAndPreClosure.isReinvestOnClosure(); + } + + public boolean isTransferToSavingsOnClosure() { + return this.accountTermAndPreClosure.isTransferToSavingsOnClosure(); + } + + public Integer getOnAccountClosureId() { + return this.accountTermAndPreClosure.getOnAccountClosureType(); + } + + public Long getTransferToSavingsAccountId() { + return this.accountTermAndPreClosure.getTransferToSavingsAccountId(); + } + + public FixedDepositAccount reInvest(BigDecimal depositAmount, ExternalId externalId) { + + final DepositAccountTermAndPreClosure newAccountTermAndPreClosure = this.accountTermAndPreClosure.copy(depositAmount); + final SavingsProduct product = this.product; + final InterestRateChart productChart = product.applicableChart(getClosedOnDate()); + final DepositAccountInterestRateChart newChart = DepositAccountInterestRateChart.from(productChart); + + final AccountType accountType = AccountType.fromInt(this.accountType); + final SavingsPostingInterestPeriodType postingPeriodType = SavingsPostingInterestPeriodType.fromInt(this.interestPostingPeriodType); + final SavingsCompoundingInterestPeriodType compoundingPeriodType = SavingsCompoundingInterestPeriodType + .fromInt(this.interestCompoundingPeriodType); + final SavingsInterestCalculationType interestCalculationType = SavingsInterestCalculationType.fromInt(this.interestCalculationType); + final SavingsInterestCalculationDaysInYearType daysInYearType = SavingsInterestCalculationDaysInYearType + .fromInt(this.interestCalculationDaysInYearType); + final BigDecimal minRequiredOpeningBalance = null; + final BigDecimal interestRate = BigDecimal.ZERO; + final Set savingsAccountCharges = null; + final SavingsPeriodFrequencyType lockinPeriodFrequencyType = SavingsPeriodFrequencyType.fromInt(this.lockinPeriodFrequencyType); + final Integer lockinPeriodFrequency = this.lockinPeriodFrequency; + final boolean withdrawalFeeApplicableForTransfer = false; + final String accountNumber = null; + final boolean withHoldTax = this.withHoldTax; + final FixedDepositAccount reInvestedAccount = FixedDepositAccount.createNewApplicationForSubmittal(client, group, product, + savingsOfficer, accountNumber, externalId, accountType, getClosedOnDate(), closedBy, interestRate, compoundingPeriodType, + postingPeriodType, interestCalculationType, daysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency, + lockinPeriodFrequencyType, withdrawalFeeApplicableForTransfer, savingsAccountCharges, newAccountTermAndPreClosure, newChart, + withHoldTax); + + newAccountTermAndPreClosure.updateAccountReference(reInvestedAccount); + newChart.updateDepositAccountReference(reInvestedAccount); + + return reInvestedAccount; + + } + + @Override + protected boolean isTransferInterestToOtherAccount() { + return this.accountTermAndPreClosure.isTransferInterestToLinkedAccount(); + } + + @Override + public boolean allowDeposit() { + return false; + } + + @Override + public boolean allowWithdrawal() { + return false; + } + + @Override + public boolean allowModify() { + return false; + } + + @Override + public boolean isTransactionsAllowed() { + return isActive() || isAccountMatured(); + } + + private boolean isAccountMatured() { + return SavingsAccountStatusType.fromInt(status).isMatured(); + } + + @Override + public BigDecimal minBalanceForInterestCalculation() { + return null; + } + + @Override + public void loadLazyCollections() { + super.loadLazyCollections(); + this.chart.getId(); + } + + public BigDecimal getDepositAmount() { + return this.accountTermAndPreClosure.depositAmount(); + } + + @Override + public BigDecimal getAccountBalance() { + return this.summary.getAccountBalance(this.currency).getAmount(); + } + + public boolean isMatured() { + return SavingsAccountStatusType.MATURED.getValue().equals(this.status); + } + + public void setClosedOnDate(final LocalDate closedOnDate) { + this.closedOnDate = closedOnDate; + } + + @Override + public DepositAccountType depositAccountType() { + return DepositAccountType.fromInt(200); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/RecurringDepositAccount.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/RecurringDepositAccount.java index 290b91fce..45f24fd81 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/RecurringDepositAccount.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/RecurringDepositAccount.java @@ -1,1269 +1,1285 @@ -/** - * 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.DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME; -import static org.apache.fineract.portfolio.savings.DepositsApiConstants.depositPeriodParamName; -import static org.apache.fineract.portfolio.savings.DepositsApiConstants.maxDepositTermParamName; -import static org.apache.fineract.portfolio.savings.DepositsApiConstants.onAccountClosureIdParamName; - -import jakarta.persistence.CascadeType; -import jakarta.persistence.DiscriminatorValue; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.OneToMany; -import jakarta.persistence.OneToOne; -import jakarta.persistence.OrderBy; -import java.math.BigDecimal; -import java.math.MathContext; -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -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.ExternalId; -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.organisation.monetary.domain.Money; -import org.apache.fineract.organisation.staff.domain.Staff; -import org.apache.fineract.portfolio.accountdetails.domain.AccountType; -import org.apache.fineract.portfolio.calendar.domain.Calendar; -import org.apache.fineract.portfolio.calendar.service.CalendarUtils; -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.interestratechart.domain.InterestRateChart; -import org.apache.fineract.portfolio.savings.DepositAccountOnClosureType; -import org.apache.fineract.portfolio.savings.DepositAccountType; -import org.apache.fineract.portfolio.savings.DepositAccountUtils; -import org.apache.fineract.portfolio.savings.DepositsApiConstants; -import org.apache.fineract.portfolio.savings.PreClosurePenalInterestOnType; -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.domain.interest.SavingsAccountTransactionDetailsForPostingPeriod; -import org.apache.fineract.portfolio.savings.service.SavingsEnumerations; -import org.apache.fineract.useradministration.domain.AppUser; - -@Entity -@DiscriminatorValue("300") -public class RecurringDepositAccount extends SavingsAccount { - - @OneToOne(mappedBy = "account", cascade = CascadeType.ALL) - private DepositAccountTermAndPreClosure accountTermAndPreClosure; - - @OneToOne(mappedBy = "account", cascade = CascadeType.ALL) - private DepositAccountRecurringDetail recurringDetail; - - @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "account") - private DepositAccountInterestRateChart chart; - - @OrderBy(value = "installmentNumber, id") - @OneToMany(cascade = CascadeType.ALL, mappedBy = "account", orphanRemoval = true, fetch = FetchType.LAZY) - private List depositScheduleInstallments = new ArrayList<>(); - - protected RecurringDepositAccount() { - // - } - - public static RecurringDepositAccount createNewApplicationForSubmittal(final Client client, final Group group, - final SavingsProduct product, final Staff fieldOfficer, final String accountNo, final ExternalId 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 savingsAccountCharges, - final DepositAccountTermAndPreClosure accountTermAndPreClosure, final DepositAccountRecurringDetail recurringDetail, - final DepositAccountInterestRateChart chart, final boolean withHoldTax) { - - final boolean allowOverdraft = false; - final BigDecimal overdraftLimit = new BigDecimal(0); - - final SavingsAccountStatusType status = SavingsAccountStatusType.SUBMITTED_AND_PENDING_APPROVAL; - return new RecurringDepositAccount(client, group, product, fieldOfficer, accountNo, externalId, status, accountType, - submittedOnDate, submittedBy, interestRate, interestCompoundingPeriodType, interestPostingPeriodType, - interestCalculationType, interestCalculationDaysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency, - lockinPeriodFrequencyType, withdrawalFeeApplicableForTransfer, savingsAccountCharges, accountTermAndPreClosure, - recurringDetail, chart, allowOverdraft, overdraftLimit, withHoldTax); - } - - public static RecurringDepositAccount createNewActivatedAccount(final Client client, final Group group, final SavingsProduct product, - final Staff fieldOfficer, final String accountNo, final ExternalId 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 savingsAccountCharges, - final DepositAccountTermAndPreClosure accountTermAndPreClosure, final DepositAccountRecurringDetail recurringDetail, - final DepositAccountInterestRateChart chart, final boolean withHoldTax) { - - final boolean allowOverdraft = false; - final BigDecimal overdraftLimit = new BigDecimal(0); - - final SavingsAccountStatusType status = SavingsAccountStatusType.ACTIVE; - return new RecurringDepositAccount(client, group, product, fieldOfficer, accountNo, externalId, status, accountType, - submittedOnDate, submittedBy, interestRate, interestCompoundingPeriodType, interestPostingPeriodType, - interestCalculationType, interestCalculationDaysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency, - lockinPeriodFrequencyType, withdrawalFeeApplicableForTransfer, savingsAccountCharges, accountTermAndPreClosure, - recurringDetail, chart, allowOverdraft, overdraftLimit, withHoldTax); - } - - private RecurringDepositAccount(final Client client, final Group group, final SavingsProduct product, final Staff fieldOfficer, - final String accountNo, final ExternalId 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 savingsAccountCharges, - final DepositAccountTermAndPreClosure accountTermAndPreClosure, final DepositAccountRecurringDetail recurringDetail, - final DepositAccountInterestRateChart chart, final boolean allowOverdraft, final BigDecimal overdraftLimit, - final boolean withHoldTax) { - - super(client, group, product, fieldOfficer, accountNo, externalId, status, accountType, submittedOnDate, submittedBy, - nominalAnnualInterestRate, interestCompoundingPeriodType, interestPostingPeriodType, interestCalculationType, - interestCalculationDaysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType, - withdrawalFeeApplicableForTransfer, savingsAccountCharges, allowOverdraft, overdraftLimit, withHoldTax); - - this.accountTermAndPreClosure = accountTermAndPreClosure; - this.recurringDetail = recurringDetail; - this.chart = chart; - if (this.chart != null) { - this.chart.updateDepositAccountReference(this); - } - } - - @Override - public void modifyApplication(final JsonCommand command, final Map actualChanges) { - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME + SavingsApiConstants.modifyApplicationAction); - super.modifyApplication(command, actualChanges, baseDataValidator); - final Map termAndPreClosureChanges = accountTermAndPreClosure.update(command, baseDataValidator); - actualChanges.putAll(termAndPreClosureChanges); - recurringDetail.update(command); - validateDomainRules(baseDataValidator); - super.validateInterestPostingAndCompoundingPeriodTypes(baseDataValidator); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - private void updateDepositAmount() { - BigDecimal recurringAmount = getRecurringDetail().mandatoryRecommendedDepositAmount(); - Integer numberOfDepositPeriods = depositScheduleInstallments().size(); - if (this.accountTermAndPreClosure.depositPeriod() != null && recurringAmount != null && numberOfDepositPeriods != null) { - BigDecimal depositAmount = Money.of(product.currency(), recurringAmount).multipliedBy(numberOfDepositPeriods) - .plus(this.minRequiredOpeningBalance).getAmount(); - accountTermAndPreClosure.updateDepositAmount(depositAmount); - } else if (accountTermAndPreClosure.depositAmount() == null) { - accountTermAndPreClosure.updateDepositAmount(Money.zero(product.currency()).getAmount()); - } - } - - public void updateDepositAmount(final BigDecimal depositAmount) { - if (this.accountTermAndPreClosure.depositPeriod() == null) { - accountTermAndPreClosure.updateDepositAmount(accountTermAndPreClosure.depositAmount().add(depositAmount)); - } - } - - @Override - protected BigDecimal getEffectiveInterestRateAsFraction(final MathContext mc, final LocalDate interestPostingUpToDate) { - boolean isPreMatureClosure = false; - return getEffectiveInterestRateAsFraction(mc, interestPostingUpToDate, isPreMatureClosure); - } - - protected BigDecimal getEffectiveInterestRateAsFraction(final MathContext mc, final LocalDate interestPostingUpToDate, - final boolean isPreMatureClosure) { - - boolean applyPreMaturePenalty = false; - BigDecimal penalInterest = BigDecimal.ZERO; - LocalDate depositCloseDate = calculateMaturityDate(); - if (isPreMatureClosure) { - if (this.accountTermAndPreClosure.isPreClosurePenalApplicable()) { - applyPreMaturePenalty = true; - penalInterest = this.accountTermAndPreClosure.depositPreClosureDetail().preClosurePenalInterest(); - final PreClosurePenalInterestOnType preClosurePenalInterestOnType = this.accountTermAndPreClosure.depositPreClosureDetail() - .preClosurePenalInterestOnType(); - if (preClosurePenalInterestOnType.isWholeTerm()) { - depositCloseDate = interestCalculatedUpto(); - } else if (preClosurePenalInterestOnType.isTillPrematureWithdrawal()) { - depositCloseDate = interestPostingUpToDate; - } - } - } - - if (depositCloseDate == null) { - depositCloseDate = DateUtils.getBusinessLocalDate(); - } - - final BigDecimal depositAmount = accountTermAndPreClosure.depositAmount(); - BigDecimal applicableInterestRate = BigDecimal.ZERO; - if (this.chart != null) { - applicableInterestRate = this.chart.getApplicableInterestRate(depositAmount, depositStartDate(), depositCloseDate, this.client); - - if (applyPreMaturePenalty) { - applicableInterestRate = applicableInterestRate.subtract(penalInterest); - applicableInterestRate = applicableInterestRate.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : applicableInterestRate; - } - } - - this.nominalAnnualInterestRate = applicableInterestRate; - - return applicableInterestRate.divide(BigDecimal.valueOf(100L), mc); - } - - public void updateMaturityDateAndAmount(final MathContext mc, final boolean isPreMatureClosure, - final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth) { - final LocalDate maturityDate = calculateMaturityDate(); - LocalDate interestCalculationUpto = null; - List allTransactions = null; - if (maturityDate == null) { - interestCalculationUpto = DateUtils.getBusinessLocalDate(); - allTransactions = getTransactions(interestCalculationUpto, false); - } else { - interestCalculationUpto = maturityDate.minusDays(1); - allTransactions = getTransactions(interestCalculationUpto, true); - - } - - final List postingPeriods = calculateInterestPayable(mc, interestCalculationUpto, allTransactions, - isPreMatureClosure, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth); - Money totalInterestPayable = Money.zero(getCurrency()); - Money totalDepositAmount = Money.zero(getCurrency()); - for (PostingPeriod postingPeriod : postingPeriods) { - totalInterestPayable = totalInterestPayable.plus(postingPeriod.getInterestEarned()); - totalDepositAmount = totalDepositAmount.plus(postingPeriod.closingBalance()).minus(postingPeriod.openingBalance()); - } - if (maturityDate == null) { - this.accountTermAndPreClosure.updateDepositAmount(totalDepositAmount.getAmount()); - } else { - this.accountTermAndPreClosure.updateMaturityDetails(totalDepositAmount.getAmount(), totalInterestPayable.getAmount(), - maturityDate); - } - } - - public void updateMaturityStatus(final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth, - final boolean postReversals) { - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME + SavingsApiConstants.updateMaturityDetailsAction); - 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 LocalDate todayDate = DateUtils.getBusinessLocalDate(); - if (!DateUtils.isAfter(this.maturityDate(), todayDate)) { - // update account status - this.status = SavingsAccountStatusType.MATURED.getValue(); - postMaturityInterest(isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, todayDate, postReversals); - } - } - - public LocalDate calculateMaturityDate() { - - final LocalDate startDate = depositStartDate(); - LocalDate maturityDate = null; - final Integer depositPeriod = this.accountTermAndPreClosure.depositPeriod(); - if (depositPeriod == null) { - return maturityDate; - } - switch (this.accountTermAndPreClosure.depositPeriodFrequencyType()) { - case DAYS: - maturityDate = startDate.plusDays(depositPeriod); - break; - case WEEKS: - maturityDate = startDate.plusWeeks(depositPeriod); - break; - case MONTHS: - maturityDate = startDate.plusMonths(depositPeriod); - break; - case YEARS: - maturityDate = startDate.plusYears(depositPeriod); - break; - case INVALID: - break; - } - - return maturityDate; - } - - private List calculateInterestPayable(final MathContext mc, final LocalDate maturityDate, - final List transactions, final boolean isPreMatureClosure, - final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth) { - - // 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); - List PostedAsOnDates = getManualPostingDates(); - final List postingPeriodIntervals = this.savingsHelper.determineInterestPostingPeriods(depositStartDate(), - maturityDate, postingPeriodType, financialYearBeginningMonth, PostedAsOnDates); - - final List allPostingPeriods = new ArrayList<>(); - - Money periodStartingBalance = Money.zero(currency); - - final SavingsInterestCalculationType interestCalculationType = SavingsInterestCalculationType.fromInt(this.interestCalculationType); - final BigDecimal interestRateAsFraction = getEffectiveInterestRateAsFraction(mc, maturityDate, isPreMatureClosure); - final Collection interestPostTransactions = this.savingsHelper.fetchPostInterestTransactionIds(getId()); - boolean isInterestTransfer = false; - final Money minBalanceForInterestCalculation = Money.of(getCurrency(), minBalanceForInterestCalculation()); - List savingsAccountTransactionDetailsForPostingPeriodList = toSavingsAccountTransactionDetailsForPostingPeriodList( - transactions); - for (final LocalDateInterval periodInterval : postingPeriodIntervals) { - boolean isUserPosting = false; - if (PostedAsOnDates.contains(periodInterval.endDate())) { - isUserPosting = true; - } - final PostingPeriod postingPeriod = PostingPeriod.createFrom(periodInterval, periodStartingBalance, - savingsAccountTransactionDetailsForPostingPeriodList, this.currency, compoundingPeriodType, interestCalculationType, - interestRateAsFraction, daysInYearType.getValue(), maturityDate, interestPostTransactions, isInterestTransfer, - minBalanceForInterestCalculation, isSavingsInterestPostingAtCurrentPeriodEnd, isUserPosting, - financialYearBeginningMonth); - - periodStartingBalance = postingPeriod.closingBalance(); - - allPostingPeriods.add(postingPeriod); - } - - this.savingsHelper.calculateInterestForAllPostingPeriods(this.currency, allPostingPeriods, this.getLockedInUntilDate(), - isTransferInterestToOtherAccount()); - // this.summary.updateFromInterestPeriodSummaries(this.currency, - // allPostingPeriods); - return allPostingPeriods; - } - - private List getTransactions(final LocalDate depositEndDate, final boolean generateFutureTransactions) { - List allTransactions = new ArrayList<>(); - // add existing transactions - allTransactions.addAll(retreiveOrderedNonInterestPostingTransactions()); - LocalDate latestTransactionDate = null; - for (final SavingsAccountTransaction installment : allTransactions) { - if (latestTransactionDate == null || DateUtils.isBefore(latestTransactionDate, installment.getTransactionDate())) { - latestTransactionDate = installment.getTransactionDate(); - } - } - String refNo = null; - if (generateFutureTransactions) { - for (RecurringDepositScheduleInstallment installment : depositScheduleInstallments()) { - if (installment.isPrincipalNotCompleted(getCurrency())) { - LocalDate dueDate = installment.dueDate(); - if (DateUtils.isBefore(dueDate, latestTransactionDate)) { - dueDate = latestTransactionDate; - } - final SavingsAccountTransaction transaction = SavingsAccountTransaction.deposit(null, office(), null, dueDate, - installment.getDepositAmountOutstanding(getCurrency()), refNo); - allTransactions.add(transaction); - } - } - } - - allTransactions = sortTransactions(allTransactions); - Money runningBalance = Money.zero(getCurrency()); - for (final SavingsAccountTransaction transaction : allTransactions) { - if (transaction.isReversed()) { - transaction.zeroBalanceFields(); - } else { - Money transactionAmount = Money.zero(this.currency); - if (transaction.isCredit()) { - transactionAmount = transactionAmount.plus(transaction.getAmount(this.currency)); - } else if (transaction.isDebit()) { - transactionAmount = transactionAmount.minus(transaction.getAmount(this.currency)); - } - - runningBalance = runningBalance.plus(transactionAmount); - transaction.setRunningBalance(runningBalance); - } - } - // loop over transactions in reverse - LocalDate endOfBalanceDate = depositEndDate; - for (int i = allTransactions.size() - 1; i >= 0; i--) { - final SavingsAccountTransaction transaction = allTransactions.get(i); - if (transaction.isNotReversed() && !transaction.isInterestPostingAndNotReversed()) { - transaction.updateCumulativeBalanceAndDates(this.currency, endOfBalanceDate); - endOfBalanceDate = transaction.getTransactionDate().minusDays(1); - } - } - return allTransactions; - } - - public LocalDate depositStartDate() { - final LocalDate depositStartDate = accountTermAndPreClosure.getExpectedFirstDepositOnDate(); - if (depositStartDate == null) { - return accountSubmittedOrActivationDate(); - } - return depositStartDate; - } - - public void prematureClosure(final AppUser currentUser, final JsonCommand command, final Map actualChanges) { - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME + DepositsApiConstants.preMatureCloseAction); - - 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 = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); - final LocalDate closedDate = command.localDateValueOfParameterNamed(SavingsApiConstants.closedOnDateParamName); - - if (DateUtils.isBefore(closedDate, getActivationDate())) { - baseDataValidator.reset().parameter(SavingsApiConstants.closedOnDateParamName).value(closedDate) - .failWithCode("must.be.after.activation.date"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - if (isAccountLocked(closedDate)) { - baseDataValidator.reset().parameter(SavingsApiConstants.closedOnDateParamName).value(closedDate) - .failWithCode("must.be.after.lockin.period"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - if (maturityDate() != null && DateUtils.isAfter(closedDate, maturityDate())) { - baseDataValidator.reset().parameter(SavingsApiConstants.closedOnDateParamName).value(closedDate) - .failWithCode("must.be.before.maturity.date"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - if (DateUtils.isAfterBusinessDate(closedDate)) { - baseDataValidator.reset().parameter(SavingsApiConstants.closedOnDateParamName).value(closedDate) - .failWithCode("cannot.be.a.future.date"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - if (isAccountLocked(calculateMaturityDate())) { - baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("deposit.period.must.be.greater.than.lock.in.period", - "Deposit period must be greater than account lock-in period."); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - final List savingsAccountTransactions = retrieveListOfTransactions(); - 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); - } - } - } - - validateActivityNotBeforeClientOrGroupTransferDate(SavingsEvent.SAVINGS_CLOSE_ACCOUNT, closedDate); - this.status = SavingsAccountStatusType.PRE_MATURE_CLOSURE.getValue(); - - final Integer onAccountClosureId = command.integerValueOfParameterNamed(onAccountClosureIdParamName); - final DepositAccountOnClosureType onClosureType = DepositAccountOnClosureType.fromInt(onAccountClosureId); - this.accountTermAndPreClosure.updateOnAccountClosureStatus(onClosureType); - - /* - * // withdraw deposit amount before closing the account final Money transactionAmountMoney = - * Money.of(this.currency, this.getAccountBalance()); final SavingsAccountTransaction withdraw = - * SavingsAccountTransaction.withdrawal(this, office(), paymentDetail, closedDate, transactionAmountMoney, new - * Date()); this.transactions.add(withdraw); - */ - 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.format(fmt)); - - this.rejectedOnDate = null; - this.rejectedBy = null; - this.withdrawnOnDate = null; - this.withdrawnBy = null; - this.closedOnDate = closedDate; - this.closedBy = currentUser; - this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, this.transactions); - - } - - @Override - public Money activateWithBalance() { - return Money.of(this.currency, this.minRequiredOpeningBalance); - } - - protected void processAccountUponActivation(final DateTimeFormatter fmt, final boolean postReversals, - final Long relaxingDaysConfigForPivotDate) { - final Money minRequiredOpeningBalance = Money.of(this.currency, this.minRequiredOpeningBalance); - final boolean backdatedTxnsAllowedTill = false; - String refNo = null; - if (minRequiredOpeningBalance.isGreaterThanZero()) { - final SavingsAccountTransactionDTO transactionDTO = new SavingsAccountTransactionDTO(fmt, getActivationDate(), - minRequiredOpeningBalance.getAmount(), null, null, accountType); - deposit(transactionDTO, backdatedTxnsAllowedTill, relaxingDaysConfigForPivotDate, refNo); - - // update existing transactions so derived balance fields are - // correct. - recalculateDailyBalances(Money.zero(this.currency), DateUtils.getBusinessLocalDate(), backdatedTxnsAllowedTill, postReversals); - } - } - - public void close(final AppUser currentUser, final JsonCommand command, final Map actualChanges) { - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME + SavingsApiConstants.closeAction); - - final SavingsAccountStatusType currentStatus = SavingsAccountStatusType.fromInt(this.status); - if (!SavingsAccountStatusType.MATURED.hasStateOf(currentStatus) && this.maturityDate() != null) { - baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("not.in.matured.state"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - final Locale locale = command.extractLocale(); - final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); - final LocalDate closedDate = command.localDateValueOfParameterNamed(SavingsApiConstants.closedOnDateParamName); - - if (DateUtils.isBefore(closedDate, getActivationDate())) { - baseDataValidator.reset().parameter(SavingsApiConstants.closedOnDateParamName).value(closedDate) - .failWithCode("must.be.after.activation.date"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - if (maturityDate() != null && DateUtils.isBefore(closedDate, maturityDate())) { - baseDataValidator.reset().parameter(SavingsApiConstants.closedOnDateParamName).value(closedDate) - .failWithCode("must.be.after.account.maturity.date"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - if (DateUtils.isAfterBusinessDate(closedDate)) { - baseDataValidator.reset().parameter(SavingsApiConstants.closedOnDateParamName).value(closedDate) - .failWithCode("cannot.be.a.future.date"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - final List savingsAccountTransactions = retrieveListOfTransactions(); - 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); - } - } - } - - validateActivityNotBeforeClientOrGroupTransferDate(SavingsEvent.SAVINGS_CLOSE_ACCOUNT, closedDate); - this.status = SavingsAccountStatusType.CLOSED.getValue(); - - final Integer onAccountClosureId = command.integerValueOfParameterNamed(onAccountClosureIdParamName); - final DepositAccountOnClosureType onClosureType = DepositAccountOnClosureType.fromInt(onAccountClosureId); - this.accountTermAndPreClosure.updateOnAccountClosureStatus(onClosureType); - - /* - * // withdraw deposit amount before closing the account final Money transactionAmountMoney = - * Money.of(this.currency, this.getAccountBalance()); final SavingsAccountTransaction withdraw = - * SavingsAccountTransaction.withdrawal(this, office(), paymentDetail, closedDate, transactionAmountMoney, new - * Date()); this.transactions.add(withdraw); - */ - - 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.format(fmt)); - - this.rejectedOnDate = null; - this.rejectedBy = null; - this.withdrawnOnDate = null; - this.withdrawnBy = null; - this.closedOnDate = closedDate; - this.closedBy = currentUser; - this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, this.transactions); - } - - public void postMaturityInterest(final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth, - final LocalDate closeDate, final boolean postReversals) { - LocalDate interestPostingUpToDate = maturityDate(); - if (interestPostingUpToDate == null) { - interestPostingUpToDate = closeDate; - } - this.setClosedOnDate(closeDate); - final MathContext mc = MathContext.DECIMAL64; - boolean isInterestTransfer = false; - LocalDate postInterestOnDate = null; - final boolean backdatedTxnsAllowedTill = false; - final List postingPeriods = calculateInterestUsing(mc, interestPostingUpToDate.minusDays(1), isInterestTransfer, - isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, - postReversals); - - Money interestPostedToDate = Money.zero(this.currency); - - boolean recalucateDailyBalanceDetails = false; - - for (final PostingPeriod interestPostingPeriod : postingPeriods) { - - LocalDate interestPostingTransactionDate = interestPostingPeriod.dateOfPostingTransaction(); - interestPostingTransactionDate = DateUtils.isAfter(interestPostingTransactionDate, interestPostingUpToDate) - ? interestPostingUpToDate - : interestPostingTransactionDate; - final Money interestEarnedToBePostedForPeriod = interestPostingPeriod.getInterestEarned(); - - interestPostedToDate = interestPostedToDate.plus(interestEarnedToBePostedForPeriod); - - final SavingsAccountTransaction postingTransaction = findInterestPostingTransactionFor(interestPostingTransactionDate); - if (postingTransaction == null) { - final SavingsAccountTransaction newPostingTransaction = SavingsAccountTransaction.interestPosting(this, office(), - interestPostingTransactionDate, interestEarnedToBePostedForPeriod, interestPostingPeriod.isUserPosting()); - addTransaction(newPostingTransaction); - recalucateDailyBalanceDetails = true; - } else { - final boolean correctionRequired = postingTransaction.hasNotAmount(interestEarnedToBePostedForPeriod); - if (correctionRequired) { - postingTransaction.reverse(); - final SavingsAccountTransaction newPostingTransaction = SavingsAccountTransaction.interestPosting(this, office(), - interestPostingTransactionDate, interestEarnedToBePostedForPeriod, interestPostingPeriod.isUserPosting()); - addTransaction(newPostingTransaction); - recalucateDailyBalanceDetails = true; - } - } - } - applyWithholdTaxForDepositAccounts(interestPostingUpToDate, recalucateDailyBalanceDetails, backdatedTxnsAllowedTill); - if (recalucateDailyBalanceDetails) { - // update existing transactions so derived balance fields are - // correct. - recalculateDailyBalances(Money.zero(this.currency), interestPostingUpToDate, backdatedTxnsAllowedTill, postReversals); - } - this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, this.transactions); - } - - public void postPreMaturityInterest(final LocalDate accountCloseDate, final boolean isPreMatureClosure, - final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth, boolean postReversals) { - - final Money interestPostedToDate = totalInterestPosted(); - // calculate interest before one day of closure date - final LocalDate interestCalculatedToDate = accountCloseDate.minusDays(1); - final Money interestOnMaturity = calculatePreMatureInterest(interestCalculatedToDate, - retreiveOrderedNonInterestPostingTransactions(), isPreMatureClosure, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth); - - boolean recalucateDailyBalance = false; - final boolean backdatedTxnsAllowedTill = false; - - // post remaining interest - final Money remainigInterestToBePosted = interestOnMaturity.minus(interestPostedToDate); - if (!remainigInterestToBePosted.isZero()) { - final boolean postInterestAsOn = false; - final SavingsAccountTransaction newPostingTransaction = SavingsAccountTransaction.interestPosting(this, office(), - accountCloseDate, remainigInterestToBePosted, postInterestAsOn); - addTransaction(newPostingTransaction); - recalucateDailyBalance = true; - } - - applyWithholdTaxForDepositAccounts(accountCloseDate, recalucateDailyBalance, backdatedTxnsAllowedTill); - if (recalucateDailyBalance) { - // update existing transactions so derived balance fields are - // correct. - recalculateDailyBalances(Money.zero(this.currency), accountCloseDate, backdatedTxnsAllowedTill, postReversals); - } - - this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, this.transactions); - this.accountTermAndPreClosure.updateMaturityDetails(this.getAccountBalance(), accountCloseDate); - } - - public BigDecimal calculatePreMatureAmount(final LocalDate preMatureDate, final boolean isPreMatureClosure, - final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth) { - - final Money interestPostedToDate = totalInterestPosted().copy(); - - final Money interestEarnedTillDate = calculatePreMatureInterest(preMatureDate, retreiveOrderedNonInterestPostingTransactions(), - isPreMatureClosure, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth); - - final Money accountBalance = Money.of(getCurrency(), getAccountBalance()); - final Money maturityAmount = accountBalance.minus(interestPostedToDate).plus(interestEarnedTillDate); - - return maturityAmount.getAmount(); - } - - private Money calculatePreMatureInterest(final LocalDate preMatureDate, final List transactions, - final boolean isPreMatureClosure, final boolean isSavingsInterestPostingAtCurrentPeriodEnd, - final Integer financialYearBeginningMonth) { - final MathContext mc = MathContext.DECIMAL64; - final List postingPeriods = calculateInterestPayable(mc, preMatureDate, transactions, isPreMatureClosure, - isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth); - - Money interestOnMaturity = Money.zero(this.currency); - - for (final PostingPeriod interestPostingPeriod : postingPeriods) { - final Money interestEarnedForPeriod = interestPostingPeriod.getInterestEarned(); - interestOnMaturity = interestOnMaturity.plus(interestEarnedForPeriod); - } - this.summary.updateFromInterestPeriodSummaries(this.currency, postingPeriods); - return interestOnMaturity; - } - - @Override - public void postInterest(final MathContext mc, final LocalDate postingDate, final boolean isInterestTransfer, - final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth, - final LocalDate postInterestAson, final boolean backdatedTxnsAllowedTill, final boolean postReversals) { - final LocalDate interestPostingUpToDate = interestPostingUpToDate(postingDate); - super.postInterest(mc, interestPostingUpToDate, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestAson, backdatedTxnsAllowedTill, postReversals); - } - - @Override - public List calculateInterestUsing(final MathContext mc, final LocalDate postingDate, boolean isInterestTransfer, - final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth, - final LocalDate postAsInterestOn, final boolean backdatedTxnsAllowedTill, final boolean postReversals) { - final LocalDate interestPostingUpToDate = interestPostingUpToDate(postingDate); - return super.calculateInterestUsing(mc, interestPostingUpToDate, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postAsInterestOn, backdatedTxnsAllowedTill, postReversals); - } - - private LocalDate interestPostingUpToDate(final LocalDate interestPostingDate) { - LocalDate interestPostingUpToDate = interestPostingDate; - final LocalDate uptoMaturityDate = interestCalculatedUpto(); - if (uptoMaturityDate != null && DateUtils.isBefore(uptoMaturityDate, interestPostingDate)) { - interestPostingUpToDate = uptoMaturityDate; - } - return interestPostingUpToDate; - } - - public LocalDate maturityDate() { - return this.accountTermAndPreClosure.getMaturityDate(); - } - - public BigDecimal maturityAmount() { - return this.accountTermAndPreClosure.maturityAmount(); - } - - private LocalDate interestCalculatedUpto() { - LocalDate uptoMaturityDate = calculateMaturityDate(); - if (uptoMaturityDate != null) { - // interest should not be calculated for maturity day - uptoMaturityDate = uptoMaturityDate.minusDays(1); - } - return uptoMaturityDate; - } - - private Money totalInterestPosted() { - Money interestPostedToDate = Money.zero(this.currency); - List trans = getTransactions(); - for (final SavingsAccountTransaction transaction : trans) { - if (transaction.isInterestPostingAndNotReversed()) { - interestPostedToDate = interestPostedToDate.plus(transaction.getAmount(currency)); - } - } - - return interestPostedToDate; - } - - @Override - public Map activate(final AppUser currentUser, final JsonCommand command) { - final Map actualChanges = super.activate(currentUser, command); - - if (accountTermAndPreClosure.isAfterExpectedFirstDepositDate(getActivationDate())) { - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME); - final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(command.extractLocale()); - final String dateAsString = formatter.format(this.accountTermAndPreClosure.getExpectedFirstDepositOnDate()); - baseDataValidator.reset().parameter(DepositsApiConstants.activatedOnDateParamName).value(dateAsString) - .failWithCodeNoParameterAddedToErrorCode("cannot.be.before.expected.first.deposit.date"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - return actualChanges; - } - - protected List sortTransactions(final List transactions) { - final List listOfTransactionsSorted = new ArrayList<>(); - listOfTransactionsSorted.addAll(transactions); - - final SavingsAccountTransactionComparator transactionComparator = new SavingsAccountTransactionComparator(); - Collections.sort(listOfTransactionsSorted, transactionComparator); - return listOfTransactionsSorted; - } - - @Override - public SavingsAccountTransaction deposit(final SavingsAccountTransactionDTO transactionDTO, final boolean backdatedTxnsAllowedTill, - final Long relaxingDaysConfigForPivotDate, final String refNo) { - - if (isAccountMatured()) { - final String defaultUserMessage = "Transaction is not allowed. Account is matured."; - final ApiParameterError error = ApiParameterError.parameterError( - "error.msg.recurring.deposit.account.transaction.account.is.matured", defaultUserMessage, "transactionDate", - transactionDTO.getTransactionDate().format(transactionDTO.getFormatter())); - - final List dataValidationErrors = new ArrayList<>(); - dataValidationErrors.add(error); - - throw new PlatformApiDataValidationException(dataValidationErrors); - } - - if (!isBeforeMaturityDate(transactionDTO.getTransactionDate())) { - final String defaultUserMessage = "Transaction is not allowed. Transaction date is on or after account maturity date."; - final ApiParameterError error = ApiParameterError.parameterError( - "error.msg.recurring.deposit.account.transaction.date.is.after.account.maturity.date", defaultUserMessage, - "transactionDate", transactionDTO.getTransactionDate().format(transactionDTO.getFormatter())); - - final List dataValidationErrors = new ArrayList<>(); - dataValidationErrors.add(error); - - throw new PlatformApiDataValidationException(dataValidationErrors); - } - - if (isBeforeDepositStartDate(transactionDTO.getTransactionDate())) { - final String defaultUserMessage = "Transaction is not allowed. Transaction date is on or after account activation and deposit start date."; - final ApiParameterError error = ApiParameterError.parameterError( - "error.msg.recurring.deposit.account.transaction.date.is.before.account.activation.or.deposit.date", defaultUserMessage, - "transactionDate", transactionDTO.getTransactionDate().format(transactionDTO.getFormatter())); - - final List dataValidationErrors = new ArrayList<>(); - dataValidationErrors.add(error); - - throw new PlatformApiDataValidationException(dataValidationErrors); - } - - final SavingsAccountTransaction transaction = super.deposit(transactionDTO, backdatedTxnsAllowedTill, - relaxingDaysConfigForPivotDate, refNo); - - return transaction; - } - - public void handleScheduleInstallments(final SavingsAccountTransaction transaction) { - final LocalDate transactionDate = transaction.getTransactionDate(); - Money transactionAmountUnprocessed = transaction.getAmount(getCurrency()); - - for (RecurringDepositScheduleInstallment currentInstallment : depositScheduleInstallments()) { - if (currentInstallment.isNotFullyPaidOff() && transactionAmountUnprocessed.isGreaterThanZero()) { - if (!this.adjustAdvanceTowardsFuturePayments() && DateUtils.isBefore(transactionDate, currentInstallment.dueDate())) { - transactionAmountUnprocessed = Money.zero(getCurrency()); - } - transactionAmountUnprocessed = handleInstallmentTransaction(currentInstallment, transactionAmountUnprocessed, - transactionDate); - } - } - - } - - public void updateScheduleInstallments() { - // reset all installments to process from the beginning - for (RecurringDepositScheduleInstallment currentInstallment : depositScheduleInstallments()) { - currentInstallment.resetDerivedFields(); - } - - final List orderedDepositTransactions = retreiveOrderedDepositTransactions(); - for (SavingsAccountTransaction transaction : orderedDepositTransactions) { - handleScheduleInstallments(transaction); - } - } - - public void updateScheduleInstallmentsWithNewRecommendedDepositAmount(BigDecimal newDepositAmount, - LocalDate depositAmountupdatedFromDate) { - // reset all installments to process from the beginning, also update - // deposit amount as necessary - for (RecurringDepositScheduleInstallment currentInstallment : depositScheduleInstallments()) { - if (!DateUtils.isAfter(depositAmountupdatedFromDate, currentInstallment.dueDate())) { - currentInstallment.updateDepositAmountAndResetDerivedFields(newDepositAmount); - } else { - currentInstallment.resetDerivedFields(); - } - } - - final List orderedDepositTransactions = retreiveOrderedDepositTransactions(); - for (SavingsAccountTransaction transaction : orderedDepositTransactions) { - handleScheduleInstallments(transaction); - } - } - - private List retreiveOrderedDepositTransactions() { - final List listOfTransactionsSorted = retrieveListOfTransactions(); - - final List orderedDepositTransactions = new ArrayList<>(); - - for (final SavingsAccountTransaction transaction : listOfTransactionsSorted) { - if (transaction.isDepositAndNotReversed()) { - orderedDepositTransactions.add(transaction); - } - } - - return orderedDepositTransactions; - } - - /** - * This method is responsible for checking if the current transaction is 'an advance/early payment' based on the - * details passed through. - * - * Default implementation is check transaction date is before installment due date. - */ - protected boolean isTransactionInAdvanceOfInstallment(final int currentInstallmentIndex, - final List installments, final LocalDate transactionDate) { - final RecurringDepositScheduleInstallment currentInstallment = installments.get(currentInstallmentIndex); - return DateUtils.isBefore(transactionDate, currentInstallment.dueDate()); - } - - private Money handleInstallmentTransaction(final RecurringDepositScheduleInstallment currentInstallment, - final Money transactionAmountUnprocessed, final LocalDate transactionDate) { - Money transactionAmountRemaining = transactionAmountUnprocessed; - Money depositAmountPortion = currentInstallment.payInstallment(transactionDate, transactionAmountRemaining); - transactionAmountRemaining = transactionAmountRemaining.minus(depositAmountPortion); - - return transactionAmountRemaining; - - } - - private boolean isAccountMatured() { - return SavingsAccountStatusType.fromInt(status).isMatured(); - } - - private boolean isBeforeMaturityDate(final LocalDate compareDate) { - final LocalDate maturityDate = this.maturityDate(); - return maturityDate == null || DateUtils.isBefore(compareDate, maturityDate); - } - - private boolean isBeforeDepositStartDate(LocalDate compareDate) { - return DateUtils.isBefore(compareDate, depositStartDate()); - } - - public void validateDomainRules() { - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME); - validateDomainRules(baseDataValidator); - super.validateInterestPostingAndCompoundingPeriodTypes(baseDataValidator); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - private void validateDomainRules(final DataValidatorBuilder baseDataValidator) { - final boolean isMinTermGreaterThanMax = this.accountTermAndPreClosure.depositTermDetail() - .isMinDepositTermGreaterThanMaxDepositTerm(); - // deposit period should be within min and max deposit term - if (isMinTermGreaterThanMax) { - final Integer maxTerm = this.accountTermAndPreClosure.depositTermDetail().maxDepositTerm(); - baseDataValidator.reset().parameter(maxDepositTermParamName).value(maxTerm) - .failWithCodeNoParameterAddedToErrorCode("max.term.lessthan.min.term"); - } - - final Integer depositPeriod = this.accountTermAndPreClosure.depositPeriod(); - if (this.accountTermAndPreClosure.depositTermDetail().maxDepositTerm() != null) { - baseDataValidator.reset().parameter(depositPeriodParamName).value(depositPeriod).notNull(); - } - if (depositPeriod != null) { - final SavingsPeriodFrequencyType depositPeriodFrequencyType = this.accountTermAndPreClosure.depositPeriodFrequencyType(); - final boolean isValidDepositPeriod = this.accountTermAndPreClosure.depositTermDetail() - .isDepositBetweenMinAndMax(depositStartDate(), calculateMaturityDate()); - if (!isValidDepositPeriod) { - baseDataValidator.reset().parameter(depositPeriodParamName).value(depositPeriod) - .failWithCodeNoParameterAddedToErrorCode("deposit.period.not.between.min.and.max.deposit.term"); - } else { - final Integer inMultiplesOf = this.accountTermAndPreClosure.depositTermDetail().inMultiplesOfDepositTerm(); - if (inMultiplesOf != null) { - final boolean isValid = this.accountTermAndPreClosure.depositTermDetail().isValidInMultiplesOfPeriod(depositPeriod, - depositPeriodFrequencyType); - if (!isValid) { - baseDataValidator.reset().parameter(depositPeriodParamName).value(depositPeriod) - .failWithCodeNoParameterAddedToErrorCode("deposit.period.not.multiple.of.term"); - } - } - } - if (isAccountLocked(calculateMaturityDate())) { - baseDataValidator.reset().parameter(depositPeriodParamName).value(depositPeriod).failWithCode( - "deposit.period.must.be.greater.than.lock.in.period", - "Deposit period must be greater than account lock-in period."); - } - - } - if (firstDepositDateBeforeAccountSubmittedOrActivationDate()) { - baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode( - "expected.first.deposit.date.must.be.after.account.submitted.or.activation.date"); - } - - // FIXME: Handle this scenario - /* - * //final boolean recurringFrequencyBeforeDepositPeriod = recurringFrequencyBeforeDepositPeriod(); - * - * if (!recurringFrequencyBeforeDepositPeriod) { - * baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode( - * "recurring.frequency.not.before.deposit.period"); } - */ - } - - public void validateApplicableInterestRate() { - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME); - LocalDate maturityDate = calculateMaturityDate(); - if (this.chart != null) { - final LocalDate chartFromDate = this.chart.getFromDate(); - LocalDate chartEndDate = this.chart.getEndDate(); - chartEndDate = chartEndDate == null ? DateUtils.getBusinessLocalDate() : chartEndDate; - - final LocalDateInterval chartInterval = LocalDateInterval.create(chartFromDate, chartEndDate); - if (!chartInterval.contains(accountSubmittedOrActivationDate())) { - baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("no.valid.interest.rate.slab.available.for.date.range"); - } - - if (maturityDate == null) { - maturityDate = DateUtils.getBusinessLocalDate(); - } - - final BigDecimal maturityAmount = this.accountTermAndPreClosure.depositAmount(); - BigDecimal applicableInterestRate = this.chart.getApplicableInterestRate(maturityAmount, depositStartDate(), maturityDate, - this.client); - - if (applicableInterestRate.compareTo(BigDecimal.ZERO) == 0) { - baseDataValidator.reset() - .failWithCodeNoParameterAddedToErrorCode("no.applicable.interest.rate.is.found.based.on.amount.and.deposit.period"); - } - - } - // CBS: chart is optional — CBS base interest rate chart is used instead - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - /* - * final boolean recurringFrequencyBeforeDepositPeriod = recurringFrequencyBeforeDepositPeriod(); - * - * if (!recurringFrequencyBeforeDepositPeriod) { - * baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode( - * "recurring.frequency.not.before.deposit.period"); } - */ - } - - public boolean isReinvestOnClosure() { - return this.accountTermAndPreClosure.isReinvestOnClosure(); - } - - public boolean isTransferToSavingsOnClosure() { - return this.accountTermAndPreClosure.isTransferToSavingsOnClosure(); - } - - public RecurringDepositAccount reInvest(BigDecimal depositAmount) { - - final DepositAccountTermAndPreClosure newAccountTermAndPreClosure = this.accountTermAndPreClosure.copy(depositAmount); - final DepositAccountRecurringDetail recurringDetail = this.recurringDetail.copy(); - final SavingsProduct product = this.product; - final InterestRateChart productChart = product.applicableChart(getClosedOnDate()); - final DepositAccountInterestRateChart newChart = DepositAccountInterestRateChart.from(productChart); - final String accountNumber = null; - final ExternalId externalId = this.externalId; - final AccountType accountType = AccountType.fromInt(this.accountType); - final SavingsPostingInterestPeriodType postingPeriodType = SavingsPostingInterestPeriodType.fromInt(this.interestPostingPeriodType); - final SavingsCompoundingInterestPeriodType compoundingPeriodType = SavingsCompoundingInterestPeriodType - .fromInt(this.interestCompoundingPeriodType); - final SavingsInterestCalculationType interestCalculationType = SavingsInterestCalculationType.fromInt(this.interestCalculationType); - final SavingsInterestCalculationDaysInYearType daysInYearType = SavingsInterestCalculationDaysInYearType - .fromInt(this.interestCalculationDaysInYearType); - final BigDecimal minRequiredOpeningBalance = depositAmount; - final BigDecimal interestRate = BigDecimal.ZERO; - final Set savingsAccountCharges = null; - final SavingsPeriodFrequencyType lockinPeriodFrequencyType = SavingsPeriodFrequencyType.fromInt(this.lockinPeriodFrequencyType); - final Integer lockinPeriodFrequency = this.lockinPeriodFrequency; - final boolean withdrawalFeeApplicableForTransfer = false; - - LocalDate now = getClosedOnDate(); - newAccountTermAndPreClosure.updateExpectedFirstDepositDate(now); - - RecurringDepositAccount rdAccount = RecurringDepositAccount.createNewActivatedAccount(client, group, product, savingsOfficer, - accountNumber, externalId, accountType, getClosedOnDate(), closedBy, interestRate, compoundingPeriodType, postingPeriodType, - interestCalculationType, daysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType, - withdrawalFeeApplicableForTransfer, savingsAccountCharges, newAccountTermAndPreClosure, recurringDetail, newChart, - withHoldTax); - - rdAccount.setDatesFrom(now); - newAccountTermAndPreClosure.updateAccountReference(rdAccount); - recurringDetail.updateAccountReference(rdAccount); - - return rdAccount; - - } - - private boolean firstDepositDateBeforeAccountSubmittedOrActivationDate() { - final LocalDate expectedFirstDepositLocalDate = accountTermAndPreClosure.getExpectedFirstDepositOnDate(); - return expectedFirstDepositLocalDate != null - && DateUtils.isBefore(expectedFirstDepositLocalDate, accountSubmittedOrActivationDate()); - } - - public void setDatesFrom(final LocalDate now) { - this.rejectedOnDate = null; - this.rejectedBy = null; - this.withdrawnOnDate = null; - this.withdrawnBy = null; - this.closedOnDate = null; - this.closedBy = null; - - this.activatedBy = null; - this.lockedInUntilDate = null; - - this.activatedOnDate = now; - } - - public void setClosedOnDate(final LocalDate closedOnDate) { - this.closedOnDate = closedOnDate; - } - - @Override - protected boolean isTransferInterestToOtherAccount() { - return this.accountTermAndPreClosure.isTransferInterestToLinkedAccount(); - } - - public void generateSchedule(final PeriodFrequencyType frequency, final Integer recurringEvery, final Calendar calendar) { - this.depositScheduleInstallments.clear(); - LocalDate installmentDate = null; - if (this.isCalendarInherited()) { - installmentDate = CalendarUtils.getNextScheduleDate(calendar, accountSubmittedOrActivationDate()); - } else { - installmentDate = depositStartDate(); - } - - int installmentNumber = 1; - final LocalDate maturityDate = calcualteScheduleTillDate(frequency, recurringEvery); - final BigDecimal depositAmount = this.recurringDetail.mandatoryRecommendedDepositAmount(); - while (DateUtils.isBefore(installmentDate, maturityDate)) { - final RecurringDepositScheduleInstallment installment = RecurringDepositScheduleInstallment.installment(this, installmentNumber, - installmentDate, depositAmount); - addDepositScheduleInstallment(installment); - installmentDate = DepositAccountUtils.calculateNextDepositDate(installmentDate, frequency, recurringEvery); - installmentNumber += 1; - } - updateDepositAmount(); - } - - private LocalDate calcualteScheduleTillDate(final PeriodFrequencyType frequency, final Integer recurringEvery) { - LocalDate tillDate = calculateMaturityDate(); - if (tillDate == null) { - final LocalDate today = DateUtils.getBusinessLocalDate(); - tillDate = DepositAccountUtils.calculateNextDepositDate(today, frequency, - recurringEvery * (DepositAccountUtils.GENERATE_MINIMUM_NUMBER_OF_FUTURE_INSTALMENTS + 1)); - } - return tillDate; - } - - private List depositScheduleInstallments() { - return this.depositScheduleInstallments; - } - - private void addDepositScheduleInstallment(final RecurringDepositScheduleInstallment installment) { - this.depositScheduleInstallments.add(installment); - } - - public boolean isCalendarInherited() { - return this.recurringDetail.isCalendarInherited(); - } - - public void updateOverduePayments(final LocalDate todayDate) { - LocalDate overdueUptoDate = this.maturityDate(); - if (overdueUptoDate == null || DateUtils.isAfter(overdueUptoDate, todayDate)) { - overdueUptoDate = todayDate; - } - - final List installments = depositScheduleInstallments(); - int noOfOverdueInstallments = 0; - Money totalOverdueAmount = Money.zero(getCurrency()); - for (RecurringDepositScheduleInstallment installment : installments) { - if (installment.isNotFullyPaidOff() && DateUtils.isAfter(overdueUptoDate, installment.dueDate())) { - noOfOverdueInstallments++; - totalOverdueAmount = totalOverdueAmount.plus(installment.getDepositAmountOutstanding(getCurrency())); - } - } - this.recurringDetail.updateOverdueDetails(noOfOverdueInstallments, totalOverdueAmount); - } - - @Override - public boolean allowWithdrawal() { - return this.recurringDetail.allowWithdrawal(); - } - - public boolean adjustAdvanceTowardsFuturePayments() { - return this.recurringDetail.adjustAdvanceTowardsFuturePayments(); - } - - @Override - public boolean isTransactionsAllowed() { - return isActive() || isAccountMatured(); - } - - public DepositAccountRecurringDetail getRecurringDetail() { - return this.recurringDetail; - } - - static class RecurringDepositScheduleInstallmentComparator implements Comparator { - - @Override - public int compare(final RecurringDepositScheduleInstallment o1, final RecurringDepositScheduleInstallment o2) { - return o1.installmentNumber().compareTo(o2.installmentNumber()); - } - } - - @Override - public void loadLazyCollections() { - this.depositScheduleInstallments.size(); - super.loadLazyCollections(); - this.chart.getId(); - } - - public BigDecimal getDepositAmount() { - return this.accountTermAndPreClosure.depositAmount(); - } - - @Override - public DepositAccountType depositAccountType() { - return DepositAccountType.fromInt(300); - } -} +/** + * 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.DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME; +import static org.apache.fineract.portfolio.savings.DepositsApiConstants.depositPeriodParamName; +import static org.apache.fineract.portfolio.savings.DepositsApiConstants.maxDepositTermParamName; +import static org.apache.fineract.portfolio.savings.DepositsApiConstants.onAccountClosureIdParamName; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import jakarta.persistence.OrderBy; +import java.math.BigDecimal; +import java.math.MathContext; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +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.ExternalId; +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.organisation.monetary.domain.Money; +import org.apache.fineract.organisation.staff.domain.Staff; +import org.apache.fineract.portfolio.accountdetails.domain.AccountType; +import org.apache.fineract.portfolio.calendar.domain.Calendar; +import org.apache.fineract.portfolio.calendar.service.CalendarUtils; +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.interestratechart.domain.InterestRateChart; +import org.apache.fineract.portfolio.savings.DepositAccountOnClosureType; +import org.apache.fineract.portfolio.savings.DepositAccountType; +import org.apache.fineract.portfolio.savings.DepositAccountUtils; +import org.apache.fineract.portfolio.savings.DepositsApiConstants; +import org.apache.fineract.portfolio.savings.PreClosurePenalInterestOnType; +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.domain.interest.SavingsAccountTransactionDetailsForPostingPeriod; +import org.apache.fineract.portfolio.savings.service.SavingsEnumerations; +import org.apache.fineract.useradministration.domain.AppUser; + +@Entity +@DiscriminatorValue("300") +public class RecurringDepositAccount extends SavingsAccount { + + @OneToOne(mappedBy = "account", cascade = CascadeType.ALL) + private DepositAccountTermAndPreClosure accountTermAndPreClosure; + + @OneToOne(mappedBy = "account", cascade = CascadeType.ALL) + private DepositAccountRecurringDetail recurringDetail; + + @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "account") + private DepositAccountInterestRateChart chart; + + @OrderBy(value = "installmentNumber, id") + @OneToMany(cascade = CascadeType.ALL, mappedBy = "account", orphanRemoval = true, fetch = FetchType.LAZY) + private List depositScheduleInstallments = new ArrayList<>(); + + protected RecurringDepositAccount() { + // + } + + public static RecurringDepositAccount createNewApplicationForSubmittal(final Client client, final Group group, + final SavingsProduct product, final Staff fieldOfficer, final String accountNo, final ExternalId 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 savingsAccountCharges, + final DepositAccountTermAndPreClosure accountTermAndPreClosure, final DepositAccountRecurringDetail recurringDetail, + final DepositAccountInterestRateChart chart, final boolean withHoldTax) { + + final boolean allowOverdraft = false; + final BigDecimal overdraftLimit = new BigDecimal(0); + + final SavingsAccountStatusType status = SavingsAccountStatusType.SUBMITTED_AND_PENDING_APPROVAL; + return new RecurringDepositAccount(client, group, product, fieldOfficer, accountNo, externalId, status, accountType, + submittedOnDate, submittedBy, interestRate, interestCompoundingPeriodType, interestPostingPeriodType, + interestCalculationType, interestCalculationDaysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency, + lockinPeriodFrequencyType, withdrawalFeeApplicableForTransfer, savingsAccountCharges, accountTermAndPreClosure, + recurringDetail, chart, allowOverdraft, overdraftLimit, withHoldTax); + } + + public static RecurringDepositAccount createNewActivatedAccount(final Client client, final Group group, final SavingsProduct product, + final Staff fieldOfficer, final String accountNo, final ExternalId 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 savingsAccountCharges, + final DepositAccountTermAndPreClosure accountTermAndPreClosure, final DepositAccountRecurringDetail recurringDetail, + final DepositAccountInterestRateChart chart, final boolean withHoldTax) { + + final boolean allowOverdraft = false; + final BigDecimal overdraftLimit = new BigDecimal(0); + + final SavingsAccountStatusType status = SavingsAccountStatusType.ACTIVE; + return new RecurringDepositAccount(client, group, product, fieldOfficer, accountNo, externalId, status, accountType, + submittedOnDate, submittedBy, interestRate, interestCompoundingPeriodType, interestPostingPeriodType, + interestCalculationType, interestCalculationDaysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency, + lockinPeriodFrequencyType, withdrawalFeeApplicableForTransfer, savingsAccountCharges, accountTermAndPreClosure, + recurringDetail, chart, allowOverdraft, overdraftLimit, withHoldTax); + } + + private RecurringDepositAccount(final Client client, final Group group, final SavingsProduct product, final Staff fieldOfficer, + final String accountNo, final ExternalId 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 savingsAccountCharges, + final DepositAccountTermAndPreClosure accountTermAndPreClosure, final DepositAccountRecurringDetail recurringDetail, + final DepositAccountInterestRateChart chart, final boolean allowOverdraft, final BigDecimal overdraftLimit, + final boolean withHoldTax) { + + super(client, group, product, fieldOfficer, accountNo, externalId, status, accountType, submittedOnDate, submittedBy, + nominalAnnualInterestRate, interestCompoundingPeriodType, interestPostingPeriodType, interestCalculationType, + interestCalculationDaysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType, + withdrawalFeeApplicableForTransfer, savingsAccountCharges, allowOverdraft, overdraftLimit, withHoldTax); + + this.accountTermAndPreClosure = accountTermAndPreClosure; + this.recurringDetail = recurringDetail; + this.chart = chart; + if (this.chart != null) { + this.chart.updateDepositAccountReference(this); + } + } + + @Override + public void modifyApplication(final JsonCommand command, final Map actualChanges) { + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME + SavingsApiConstants.modifyApplicationAction); + super.modifyApplication(command, actualChanges, baseDataValidator); + final Map termAndPreClosureChanges = accountTermAndPreClosure.update(command, baseDataValidator); + actualChanges.putAll(termAndPreClosureChanges); + recurringDetail.update(command); + validateDomainRules(baseDataValidator); + super.validateInterestPostingAndCompoundingPeriodTypes(baseDataValidator); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + private void updateDepositAmount() { + BigDecimal recurringAmount = getRecurringDetail().mandatoryRecommendedDepositAmount(); + Integer numberOfDepositPeriods = depositScheduleInstallments().size(); + if (this.accountTermAndPreClosure.depositPeriod() != null && recurringAmount != null && numberOfDepositPeriods != null) { + BigDecimal depositAmount = Money.of(product.currency(), recurringAmount).multipliedBy(numberOfDepositPeriods) + .plus(this.minRequiredOpeningBalance).getAmount(); + accountTermAndPreClosure.updateDepositAmount(depositAmount); + } else if (accountTermAndPreClosure.depositAmount() == null) { + accountTermAndPreClosure.updateDepositAmount(Money.zero(product.currency()).getAmount()); + } + } + + public void updateDepositAmount(final BigDecimal depositAmount) { + if (this.accountTermAndPreClosure.depositPeriod() == null) { + accountTermAndPreClosure.updateDepositAmount(accountTermAndPreClosure.depositAmount().add(depositAmount)); + } + } + + @Override + protected BigDecimal getEffectiveInterestRateAsFraction(final MathContext mc, final LocalDate interestPostingUpToDate) { + boolean isPreMatureClosure = false; + return getEffectiveInterestRateAsFraction(mc, interestPostingUpToDate, isPreMatureClosure); + } + + protected BigDecimal getEffectiveInterestRateAsFraction(final MathContext mc, final LocalDate interestPostingUpToDate, + final boolean isPreMatureClosure) { + + boolean applyPreMaturePenalty = false; + BigDecimal penalInterest = BigDecimal.ZERO; + LocalDate depositCloseDate = calculateMaturityDate(); + if (isPreMatureClosure) { + if (this.accountTermAndPreClosure.isPreClosurePenalApplicable()) { + applyPreMaturePenalty = true; + penalInterest = this.accountTermAndPreClosure.depositPreClosureDetail().preClosurePenalInterest(); + final PreClosurePenalInterestOnType preClosurePenalInterestOnType = this.accountTermAndPreClosure.depositPreClosureDetail() + .preClosurePenalInterestOnType(); + if (preClosurePenalInterestOnType.isWholeTerm()) { + depositCloseDate = interestCalculatedUpto(); + } else if (preClosurePenalInterestOnType.isTillPrematureWithdrawal()) { + depositCloseDate = interestPostingUpToDate; + } + } + } + + if (depositCloseDate == null) { + depositCloseDate = DateUtils.getBusinessLocalDate(); + } + + final BigDecimal depositAmount = accountTermAndPreClosure.depositAmount(); + BigDecimal applicableInterestRate = BigDecimal.ZERO; + if (this.chart != null) { + applicableInterestRate = this.chart.getApplicableInterestRate(depositAmount, depositStartDate(), depositCloseDate, this.client); + + // CBS: override with senior citizen rate if applicable + if (isSeniorCitizen() && getSeniorCitizenInterestRate() != null + && getSeniorCitizenInterestRate().compareTo(BigDecimal.ZERO) > 0) { + applicableInterestRate = getSeniorCitizenInterestRate(); + } + + if (applyPreMaturePenalty) { + applicableInterestRate = applicableInterestRate.subtract(penalInterest); + applicableInterestRate = applicableInterestRate.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : applicableInterestRate; + } + } + + this.nominalAnnualInterestRate = applicableInterestRate; + + return applicableInterestRate.divide(BigDecimal.valueOf(100L), mc); + } + + @Override + public BigDecimal getEffectiveInterestRateAsFractionAccrual(final MathContext mc, final LocalDate upToInterestCalculationDate) { + // CBS: accrual must also use senior citizen rate to stay consistent with maturity calculation + if (isSeniorCitizen() && getSeniorCitizenInterestRate() != null + && getSeniorCitizenInterestRate().compareTo(BigDecimal.ZERO) > 0) { + return getSeniorCitizenInterestRate().divide(BigDecimal.valueOf(100L), mc); + } + return super.getEffectiveInterestRateAsFractionAccrual(mc, upToInterestCalculationDate); + } + + public void updateMaturityDateAndAmount(final MathContext mc, final boolean isPreMatureClosure, + final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth) { + final LocalDate maturityDate = calculateMaturityDate(); + LocalDate interestCalculationUpto = null; + List allTransactions = null; + if (maturityDate == null) { + interestCalculationUpto = DateUtils.getBusinessLocalDate(); + allTransactions = getTransactions(interestCalculationUpto, false); + } else { + interestCalculationUpto = maturityDate.minusDays(1); + allTransactions = getTransactions(interestCalculationUpto, true); + + } + + final List postingPeriods = calculateInterestPayable(mc, interestCalculationUpto, allTransactions, + isPreMatureClosure, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth); + Money totalInterestPayable = Money.zero(getCurrency()); + Money totalDepositAmount = Money.zero(getCurrency()); + for (PostingPeriod postingPeriod : postingPeriods) { + totalInterestPayable = totalInterestPayable.plus(postingPeriod.getInterestEarned()); + totalDepositAmount = totalDepositAmount.plus(postingPeriod.closingBalance()).minus(postingPeriod.openingBalance()); + } + if (maturityDate == null) { + this.accountTermAndPreClosure.updateDepositAmount(totalDepositAmount.getAmount()); + } else { + this.accountTermAndPreClosure.updateMaturityDetails(totalDepositAmount.getAmount(), totalInterestPayable.getAmount(), + maturityDate); + } + } + + public void updateMaturityStatus(final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth, + final boolean postReversals) { + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME + SavingsApiConstants.updateMaturityDetailsAction); + 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 LocalDate todayDate = DateUtils.getBusinessLocalDate(); + if (!DateUtils.isAfter(this.maturityDate(), todayDate)) { + // update account status + this.status = SavingsAccountStatusType.MATURED.getValue(); + postMaturityInterest(isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, todayDate, postReversals); + } + } + + public LocalDate calculateMaturityDate() { + + final LocalDate startDate = depositStartDate(); + LocalDate maturityDate = null; + final Integer depositPeriod = this.accountTermAndPreClosure.depositPeriod(); + if (depositPeriod == null) { + return maturityDate; + } + switch (this.accountTermAndPreClosure.depositPeriodFrequencyType()) { + case DAYS: + maturityDate = startDate.plusDays(depositPeriod); + break; + case WEEKS: + maturityDate = startDate.plusWeeks(depositPeriod); + break; + case MONTHS: + maturityDate = startDate.plusMonths(depositPeriod); + break; + case YEARS: + maturityDate = startDate.plusYears(depositPeriod); + break; + case INVALID: + break; + } + + return maturityDate; + } + + private List calculateInterestPayable(final MathContext mc, final LocalDate maturityDate, + final List transactions, final boolean isPreMatureClosure, + final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth) { + + // 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); + List PostedAsOnDates = getManualPostingDates(); + final List postingPeriodIntervals = this.savingsHelper.determineInterestPostingPeriods(depositStartDate(), + maturityDate, postingPeriodType, financialYearBeginningMonth, PostedAsOnDates); + + final List allPostingPeriods = new ArrayList<>(); + + Money periodStartingBalance = Money.zero(currency); + + final SavingsInterestCalculationType interestCalculationType = SavingsInterestCalculationType.fromInt(this.interestCalculationType); + final BigDecimal interestRateAsFraction = getEffectiveInterestRateAsFraction(mc, maturityDate, isPreMatureClosure); + final Collection interestPostTransactions = this.savingsHelper.fetchPostInterestTransactionIds(getId()); + boolean isInterestTransfer = false; + final Money minBalanceForInterestCalculation = Money.of(getCurrency(), minBalanceForInterestCalculation()); + List savingsAccountTransactionDetailsForPostingPeriodList = toSavingsAccountTransactionDetailsForPostingPeriodList( + transactions); + for (final LocalDateInterval periodInterval : postingPeriodIntervals) { + boolean isUserPosting = false; + if (PostedAsOnDates.contains(periodInterval.endDate())) { + isUserPosting = true; + } + final PostingPeriod postingPeriod = PostingPeriod.createFrom(periodInterval, periodStartingBalance, + savingsAccountTransactionDetailsForPostingPeriodList, this.currency, compoundingPeriodType, interestCalculationType, + interestRateAsFraction, daysInYearType.getValue(), maturityDate, interestPostTransactions, isInterestTransfer, + minBalanceForInterestCalculation, isSavingsInterestPostingAtCurrentPeriodEnd, isUserPosting, + financialYearBeginningMonth); + + periodStartingBalance = postingPeriod.closingBalance(); + + allPostingPeriods.add(postingPeriod); + } + + this.savingsHelper.calculateInterestForAllPostingPeriods(this.currency, allPostingPeriods, this.getLockedInUntilDate(), + isTransferInterestToOtherAccount()); + // this.summary.updateFromInterestPeriodSummaries(this.currency, + // allPostingPeriods); + return allPostingPeriods; + } + + private List getTransactions(final LocalDate depositEndDate, final boolean generateFutureTransactions) { + List allTransactions = new ArrayList<>(); + // add existing transactions + allTransactions.addAll(retreiveOrderedNonInterestPostingTransactions()); + LocalDate latestTransactionDate = null; + for (final SavingsAccountTransaction installment : allTransactions) { + if (latestTransactionDate == null || DateUtils.isBefore(latestTransactionDate, installment.getTransactionDate())) { + latestTransactionDate = installment.getTransactionDate(); + } + } + String refNo = null; + if (generateFutureTransactions) { + for (RecurringDepositScheduleInstallment installment : depositScheduleInstallments()) { + if (installment.isPrincipalNotCompleted(getCurrency())) { + LocalDate dueDate = installment.dueDate(); + if (DateUtils.isBefore(dueDate, latestTransactionDate)) { + dueDate = latestTransactionDate; + } + final SavingsAccountTransaction transaction = SavingsAccountTransaction.deposit(null, office(), null, dueDate, + installment.getDepositAmountOutstanding(getCurrency()), refNo); + allTransactions.add(transaction); + } + } + } + + allTransactions = sortTransactions(allTransactions); + Money runningBalance = Money.zero(getCurrency()); + for (final SavingsAccountTransaction transaction : allTransactions) { + if (transaction.isReversed()) { + transaction.zeroBalanceFields(); + } else { + Money transactionAmount = Money.zero(this.currency); + if (transaction.isCredit()) { + transactionAmount = transactionAmount.plus(transaction.getAmount(this.currency)); + } else if (transaction.isDebit()) { + transactionAmount = transactionAmount.minus(transaction.getAmount(this.currency)); + } + + runningBalance = runningBalance.plus(transactionAmount); + transaction.setRunningBalance(runningBalance); + } + } + // loop over transactions in reverse + LocalDate endOfBalanceDate = depositEndDate; + for (int i = allTransactions.size() - 1; i >= 0; i--) { + final SavingsAccountTransaction transaction = allTransactions.get(i); + if (transaction.isNotReversed() && !transaction.isInterestPostingAndNotReversed()) { + transaction.updateCumulativeBalanceAndDates(this.currency, endOfBalanceDate); + endOfBalanceDate = transaction.getTransactionDate().minusDays(1); + } + } + return allTransactions; + } + + public LocalDate depositStartDate() { + final LocalDate depositStartDate = accountTermAndPreClosure.getExpectedFirstDepositOnDate(); + if (depositStartDate == null) { + return accountSubmittedOrActivationDate(); + } + return depositStartDate; + } + + public void prematureClosure(final AppUser currentUser, final JsonCommand command, final Map actualChanges) { + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME + DepositsApiConstants.preMatureCloseAction); + + 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 = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); + final LocalDate closedDate = command.localDateValueOfParameterNamed(SavingsApiConstants.closedOnDateParamName); + + if (DateUtils.isBefore(closedDate, getActivationDate())) { + baseDataValidator.reset().parameter(SavingsApiConstants.closedOnDateParamName).value(closedDate) + .failWithCode("must.be.after.activation.date"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + if (isAccountLocked(closedDate)) { + baseDataValidator.reset().parameter(SavingsApiConstants.closedOnDateParamName).value(closedDate) + .failWithCode("must.be.after.lockin.period"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + if (maturityDate() != null && DateUtils.isAfter(closedDate, maturityDate())) { + baseDataValidator.reset().parameter(SavingsApiConstants.closedOnDateParamName).value(closedDate) + .failWithCode("must.be.before.maturity.date"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + if (DateUtils.isAfterBusinessDate(closedDate)) { + baseDataValidator.reset().parameter(SavingsApiConstants.closedOnDateParamName).value(closedDate) + .failWithCode("cannot.be.a.future.date"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + if (isAccountLocked(calculateMaturityDate())) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("deposit.period.must.be.greater.than.lock.in.period", + "Deposit period must be greater than account lock-in period."); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + final List savingsAccountTransactions = retrieveListOfTransactions(); + 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); + } + } + } + + validateActivityNotBeforeClientOrGroupTransferDate(SavingsEvent.SAVINGS_CLOSE_ACCOUNT, closedDate); + this.status = SavingsAccountStatusType.PRE_MATURE_CLOSURE.getValue(); + + final Integer onAccountClosureId = command.integerValueOfParameterNamed(onAccountClosureIdParamName); + final DepositAccountOnClosureType onClosureType = DepositAccountOnClosureType.fromInt(onAccountClosureId); + this.accountTermAndPreClosure.updateOnAccountClosureStatus(onClosureType); + + /* + * // withdraw deposit amount before closing the account final Money transactionAmountMoney = + * Money.of(this.currency, this.getAccountBalance()); final SavingsAccountTransaction withdraw = + * SavingsAccountTransaction.withdrawal(this, office(), paymentDetail, closedDate, transactionAmountMoney, new + * Date()); this.transactions.add(withdraw); + */ + 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.format(fmt)); + + this.rejectedOnDate = null; + this.rejectedBy = null; + this.withdrawnOnDate = null; + this.withdrawnBy = null; + this.closedOnDate = closedDate; + this.closedBy = currentUser; + this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, this.transactions); + + } + + @Override + public Money activateWithBalance() { + return Money.of(this.currency, this.minRequiredOpeningBalance); + } + + protected void processAccountUponActivation(final DateTimeFormatter fmt, final boolean postReversals, + final Long relaxingDaysConfigForPivotDate) { + final Money minRequiredOpeningBalance = Money.of(this.currency, this.minRequiredOpeningBalance); + final boolean backdatedTxnsAllowedTill = false; + String refNo = null; + if (minRequiredOpeningBalance.isGreaterThanZero()) { + final SavingsAccountTransactionDTO transactionDTO = new SavingsAccountTransactionDTO(fmt, getActivationDate(), + minRequiredOpeningBalance.getAmount(), null, null, accountType); + deposit(transactionDTO, backdatedTxnsAllowedTill, relaxingDaysConfigForPivotDate, refNo); + + // update existing transactions so derived balance fields are + // correct. + recalculateDailyBalances(Money.zero(this.currency), DateUtils.getBusinessLocalDate(), backdatedTxnsAllowedTill, postReversals); + } + } + + public void close(final AppUser currentUser, final JsonCommand command, final Map actualChanges) { + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME + SavingsApiConstants.closeAction); + + final SavingsAccountStatusType currentStatus = SavingsAccountStatusType.fromInt(this.status); + if (!SavingsAccountStatusType.MATURED.hasStateOf(currentStatus) && this.maturityDate() != null) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("not.in.matured.state"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + final Locale locale = command.extractLocale(); + final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); + final LocalDate closedDate = command.localDateValueOfParameterNamed(SavingsApiConstants.closedOnDateParamName); + + if (DateUtils.isBefore(closedDate, getActivationDate())) { + baseDataValidator.reset().parameter(SavingsApiConstants.closedOnDateParamName).value(closedDate) + .failWithCode("must.be.after.activation.date"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + if (maturityDate() != null && DateUtils.isBefore(closedDate, maturityDate())) { + baseDataValidator.reset().parameter(SavingsApiConstants.closedOnDateParamName).value(closedDate) + .failWithCode("must.be.after.account.maturity.date"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + if (DateUtils.isAfterBusinessDate(closedDate)) { + baseDataValidator.reset().parameter(SavingsApiConstants.closedOnDateParamName).value(closedDate) + .failWithCode("cannot.be.a.future.date"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + final List savingsAccountTransactions = retrieveListOfTransactions(); + 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); + } + } + } + + validateActivityNotBeforeClientOrGroupTransferDate(SavingsEvent.SAVINGS_CLOSE_ACCOUNT, closedDate); + this.status = SavingsAccountStatusType.CLOSED.getValue(); + + final Integer onAccountClosureId = command.integerValueOfParameterNamed(onAccountClosureIdParamName); + final DepositAccountOnClosureType onClosureType = DepositAccountOnClosureType.fromInt(onAccountClosureId); + this.accountTermAndPreClosure.updateOnAccountClosureStatus(onClosureType); + + /* + * // withdraw deposit amount before closing the account final Money transactionAmountMoney = + * Money.of(this.currency, this.getAccountBalance()); final SavingsAccountTransaction withdraw = + * SavingsAccountTransaction.withdrawal(this, office(), paymentDetail, closedDate, transactionAmountMoney, new + * Date()); this.transactions.add(withdraw); + */ + + 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.format(fmt)); + + this.rejectedOnDate = null; + this.rejectedBy = null; + this.withdrawnOnDate = null; + this.withdrawnBy = null; + this.closedOnDate = closedDate; + this.closedBy = currentUser; + this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, this.transactions); + } + + public void postMaturityInterest(final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth, + final LocalDate closeDate, final boolean postReversals) { + LocalDate interestPostingUpToDate = maturityDate(); + if (interestPostingUpToDate == null) { + interestPostingUpToDate = closeDate; + } + this.setClosedOnDate(closeDate); + final MathContext mc = MathContext.DECIMAL64; + boolean isInterestTransfer = false; + LocalDate postInterestOnDate = null; + final boolean backdatedTxnsAllowedTill = false; + final List postingPeriods = calculateInterestUsing(mc, interestPostingUpToDate.minusDays(1), isInterestTransfer, + isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, + postReversals); + + Money interestPostedToDate = Money.zero(this.currency); + + boolean recalucateDailyBalanceDetails = false; + + for (final PostingPeriod interestPostingPeriod : postingPeriods) { + + LocalDate interestPostingTransactionDate = interestPostingPeriod.dateOfPostingTransaction(); + interestPostingTransactionDate = DateUtils.isAfter(interestPostingTransactionDate, interestPostingUpToDate) + ? interestPostingUpToDate + : interestPostingTransactionDate; + final Money interestEarnedToBePostedForPeriod = interestPostingPeriod.getInterestEarned(); + + interestPostedToDate = interestPostedToDate.plus(interestEarnedToBePostedForPeriod); + + final SavingsAccountTransaction postingTransaction = findInterestPostingTransactionFor(interestPostingTransactionDate); + if (postingTransaction == null) { + final SavingsAccountTransaction newPostingTransaction = SavingsAccountTransaction.interestPosting(this, office(), + interestPostingTransactionDate, interestEarnedToBePostedForPeriod, interestPostingPeriod.isUserPosting()); + addTransaction(newPostingTransaction); + recalucateDailyBalanceDetails = true; + } else { + final boolean correctionRequired = postingTransaction.hasNotAmount(interestEarnedToBePostedForPeriod); + if (correctionRequired) { + postingTransaction.reverse(); + final SavingsAccountTransaction newPostingTransaction = SavingsAccountTransaction.interestPosting(this, office(), + interestPostingTransactionDate, interestEarnedToBePostedForPeriod, interestPostingPeriod.isUserPosting()); + addTransaction(newPostingTransaction); + recalucateDailyBalanceDetails = true; + } + } + } + applyWithholdTaxForDepositAccounts(interestPostingUpToDate, recalucateDailyBalanceDetails, backdatedTxnsAllowedTill); + if (recalucateDailyBalanceDetails) { + // update existing transactions so derived balance fields are + // correct. + recalculateDailyBalances(Money.zero(this.currency), interestPostingUpToDate, backdatedTxnsAllowedTill, postReversals); + } + this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, this.transactions); + } + + public void postPreMaturityInterest(final LocalDate accountCloseDate, final boolean isPreMatureClosure, + final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth, boolean postReversals) { + + final Money interestPostedToDate = totalInterestPosted(); + // calculate interest before one day of closure date + final LocalDate interestCalculatedToDate = accountCloseDate.minusDays(1); + final Money interestOnMaturity = calculatePreMatureInterest(interestCalculatedToDate, + retreiveOrderedNonInterestPostingTransactions(), isPreMatureClosure, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth); + + boolean recalucateDailyBalance = false; + final boolean backdatedTxnsAllowedTill = false; + + // post remaining interest + final Money remainigInterestToBePosted = interestOnMaturity.minus(interestPostedToDate); + if (!remainigInterestToBePosted.isZero()) { + final boolean postInterestAsOn = false; + final SavingsAccountTransaction newPostingTransaction = SavingsAccountTransaction.interestPosting(this, office(), + accountCloseDate, remainigInterestToBePosted, postInterestAsOn); + addTransaction(newPostingTransaction); + recalucateDailyBalance = true; + } + + applyWithholdTaxForDepositAccounts(accountCloseDate, recalucateDailyBalance, backdatedTxnsAllowedTill); + if (recalucateDailyBalance) { + // update existing transactions so derived balance fields are + // correct. + recalculateDailyBalances(Money.zero(this.currency), accountCloseDate, backdatedTxnsAllowedTill, postReversals); + } + + this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, this.transactions); + this.accountTermAndPreClosure.updateMaturityDetails(this.getAccountBalance(), accountCloseDate); + } + + public BigDecimal calculatePreMatureAmount(final LocalDate preMatureDate, final boolean isPreMatureClosure, + final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth) { + + final Money interestPostedToDate = totalInterestPosted().copy(); + + final Money interestEarnedTillDate = calculatePreMatureInterest(preMatureDate, retreiveOrderedNonInterestPostingTransactions(), + isPreMatureClosure, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth); + + final Money accountBalance = Money.of(getCurrency(), getAccountBalance()); + final Money maturityAmount = accountBalance.minus(interestPostedToDate).plus(interestEarnedTillDate); + + return maturityAmount.getAmount(); + } + + private Money calculatePreMatureInterest(final LocalDate preMatureDate, final List transactions, + final boolean isPreMatureClosure, final boolean isSavingsInterestPostingAtCurrentPeriodEnd, + final Integer financialYearBeginningMonth) { + final MathContext mc = MathContext.DECIMAL64; + final List postingPeriods = calculateInterestPayable(mc, preMatureDate, transactions, isPreMatureClosure, + isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth); + + Money interestOnMaturity = Money.zero(this.currency); + + for (final PostingPeriod interestPostingPeriod : postingPeriods) { + final Money interestEarnedForPeriod = interestPostingPeriod.getInterestEarned(); + interestOnMaturity = interestOnMaturity.plus(interestEarnedForPeriod); + } + this.summary.updateFromInterestPeriodSummaries(this.currency, postingPeriods); + return interestOnMaturity; + } + + @Override + public void postInterest(final MathContext mc, final LocalDate postingDate, final boolean isInterestTransfer, + final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth, + final LocalDate postInterestAson, final boolean backdatedTxnsAllowedTill, final boolean postReversals) { + final LocalDate interestPostingUpToDate = interestPostingUpToDate(postingDate); + super.postInterest(mc, interestPostingUpToDate, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth, postInterestAson, backdatedTxnsAllowedTill, postReversals); + } + + @Override + public List calculateInterestUsing(final MathContext mc, final LocalDate postingDate, boolean isInterestTransfer, + final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth, + final LocalDate postAsInterestOn, final boolean backdatedTxnsAllowedTill, final boolean postReversals) { + final LocalDate interestPostingUpToDate = interestPostingUpToDate(postingDate); + return super.calculateInterestUsing(mc, interestPostingUpToDate, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth, postAsInterestOn, backdatedTxnsAllowedTill, postReversals); + } + + private LocalDate interestPostingUpToDate(final LocalDate interestPostingDate) { + LocalDate interestPostingUpToDate = interestPostingDate; + final LocalDate uptoMaturityDate = interestCalculatedUpto(); + if (uptoMaturityDate != null && DateUtils.isBefore(uptoMaturityDate, interestPostingDate)) { + interestPostingUpToDate = uptoMaturityDate; + } + return interestPostingUpToDate; + } + + public LocalDate maturityDate() { + return this.accountTermAndPreClosure.getMaturityDate(); + } + + public BigDecimal maturityAmount() { + return this.accountTermAndPreClosure.maturityAmount(); + } + + private LocalDate interestCalculatedUpto() { + LocalDate uptoMaturityDate = calculateMaturityDate(); + if (uptoMaturityDate != null) { + // interest should not be calculated for maturity day + uptoMaturityDate = uptoMaturityDate.minusDays(1); + } + return uptoMaturityDate; + } + + private Money totalInterestPosted() { + Money interestPostedToDate = Money.zero(this.currency); + List trans = getTransactions(); + for (final SavingsAccountTransaction transaction : trans) { + if (transaction.isInterestPostingAndNotReversed()) { + interestPostedToDate = interestPostedToDate.plus(transaction.getAmount(currency)); + } + } + + return interestPostedToDate; + } + + @Override + public Map activate(final AppUser currentUser, final JsonCommand command) { + final Map actualChanges = super.activate(currentUser, command); + + if (accountTermAndPreClosure.isAfterExpectedFirstDepositDate(getActivationDate())) { + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME); + final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(command.extractLocale()); + final String dateAsString = formatter.format(this.accountTermAndPreClosure.getExpectedFirstDepositOnDate()); + baseDataValidator.reset().parameter(DepositsApiConstants.activatedOnDateParamName).value(dateAsString) + .failWithCodeNoParameterAddedToErrorCode("cannot.be.before.expected.first.deposit.date"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + return actualChanges; + } + + protected List sortTransactions(final List transactions) { + final List listOfTransactionsSorted = new ArrayList<>(); + listOfTransactionsSorted.addAll(transactions); + + final SavingsAccountTransactionComparator transactionComparator = new SavingsAccountTransactionComparator(); + Collections.sort(listOfTransactionsSorted, transactionComparator); + return listOfTransactionsSorted; + } + + @Override + public SavingsAccountTransaction deposit(final SavingsAccountTransactionDTO transactionDTO, final boolean backdatedTxnsAllowedTill, + final Long relaxingDaysConfigForPivotDate, final String refNo) { + + if (isAccountMatured()) { + final String defaultUserMessage = "Transaction is not allowed. Account is matured."; + final ApiParameterError error = ApiParameterError.parameterError( + "error.msg.recurring.deposit.account.transaction.account.is.matured", defaultUserMessage, "transactionDate", + transactionDTO.getTransactionDate().format(transactionDTO.getFormatter())); + + final List dataValidationErrors = new ArrayList<>(); + dataValidationErrors.add(error); + + throw new PlatformApiDataValidationException(dataValidationErrors); + } + + if (!isBeforeMaturityDate(transactionDTO.getTransactionDate())) { + final String defaultUserMessage = "Transaction is not allowed. Transaction date is on or after account maturity date."; + final ApiParameterError error = ApiParameterError.parameterError( + "error.msg.recurring.deposit.account.transaction.date.is.after.account.maturity.date", defaultUserMessage, + "transactionDate", transactionDTO.getTransactionDate().format(transactionDTO.getFormatter())); + + final List dataValidationErrors = new ArrayList<>(); + dataValidationErrors.add(error); + + throw new PlatformApiDataValidationException(dataValidationErrors); + } + + if (isBeforeDepositStartDate(transactionDTO.getTransactionDate())) { + final String defaultUserMessage = "Transaction is not allowed. Transaction date is on or after account activation and deposit start date."; + final ApiParameterError error = ApiParameterError.parameterError( + "error.msg.recurring.deposit.account.transaction.date.is.before.account.activation.or.deposit.date", defaultUserMessage, + "transactionDate", transactionDTO.getTransactionDate().format(transactionDTO.getFormatter())); + + final List dataValidationErrors = new ArrayList<>(); + dataValidationErrors.add(error); + + throw new PlatformApiDataValidationException(dataValidationErrors); + } + + final SavingsAccountTransaction transaction = super.deposit(transactionDTO, backdatedTxnsAllowedTill, + relaxingDaysConfigForPivotDate, refNo); + + return transaction; + } + + public void handleScheduleInstallments(final SavingsAccountTransaction transaction) { + final LocalDate transactionDate = transaction.getTransactionDate(); + Money transactionAmountUnprocessed = transaction.getAmount(getCurrency()); + + for (RecurringDepositScheduleInstallment currentInstallment : depositScheduleInstallments()) { + if (currentInstallment.isNotFullyPaidOff() && transactionAmountUnprocessed.isGreaterThanZero()) { + if (!this.adjustAdvanceTowardsFuturePayments() && DateUtils.isBefore(transactionDate, currentInstallment.dueDate())) { + transactionAmountUnprocessed = Money.zero(getCurrency()); + } + transactionAmountUnprocessed = handleInstallmentTransaction(currentInstallment, transactionAmountUnprocessed, + transactionDate); + } + } + + } + + public void updateScheduleInstallments() { + // reset all installments to process from the beginning + for (RecurringDepositScheduleInstallment currentInstallment : depositScheduleInstallments()) { + currentInstallment.resetDerivedFields(); + } + + final List orderedDepositTransactions = retreiveOrderedDepositTransactions(); + for (SavingsAccountTransaction transaction : orderedDepositTransactions) { + handleScheduleInstallments(transaction); + } + } + + public void updateScheduleInstallmentsWithNewRecommendedDepositAmount(BigDecimal newDepositAmount, + LocalDate depositAmountupdatedFromDate) { + // reset all installments to process from the beginning, also update + // deposit amount as necessary + for (RecurringDepositScheduleInstallment currentInstallment : depositScheduleInstallments()) { + if (!DateUtils.isAfter(depositAmountupdatedFromDate, currentInstallment.dueDate())) { + currentInstallment.updateDepositAmountAndResetDerivedFields(newDepositAmount); + } else { + currentInstallment.resetDerivedFields(); + } + } + + final List orderedDepositTransactions = retreiveOrderedDepositTransactions(); + for (SavingsAccountTransaction transaction : orderedDepositTransactions) { + handleScheduleInstallments(transaction); + } + } + + private List retreiveOrderedDepositTransactions() { + final List listOfTransactionsSorted = retrieveListOfTransactions(); + + final List orderedDepositTransactions = new ArrayList<>(); + + for (final SavingsAccountTransaction transaction : listOfTransactionsSorted) { + if (transaction.isDepositAndNotReversed()) { + orderedDepositTransactions.add(transaction); + } + } + + return orderedDepositTransactions; + } + + /** + * This method is responsible for checking if the current transaction is 'an advance/early payment' based on the + * details passed through. + * + * Default implementation is check transaction date is before installment due date. + */ + protected boolean isTransactionInAdvanceOfInstallment(final int currentInstallmentIndex, + final List installments, final LocalDate transactionDate) { + final RecurringDepositScheduleInstallment currentInstallment = installments.get(currentInstallmentIndex); + return DateUtils.isBefore(transactionDate, currentInstallment.dueDate()); + } + + private Money handleInstallmentTransaction(final RecurringDepositScheduleInstallment currentInstallment, + final Money transactionAmountUnprocessed, final LocalDate transactionDate) { + Money transactionAmountRemaining = transactionAmountUnprocessed; + Money depositAmountPortion = currentInstallment.payInstallment(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(depositAmountPortion); + + return transactionAmountRemaining; + + } + + private boolean isAccountMatured() { + return SavingsAccountStatusType.fromInt(status).isMatured(); + } + + private boolean isBeforeMaturityDate(final LocalDate compareDate) { + final LocalDate maturityDate = this.maturityDate(); + return maturityDate == null || DateUtils.isBefore(compareDate, maturityDate); + } + + private boolean isBeforeDepositStartDate(LocalDate compareDate) { + return DateUtils.isBefore(compareDate, depositStartDate()); + } + + public void validateDomainRules() { + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME); + validateDomainRules(baseDataValidator); + super.validateInterestPostingAndCompoundingPeriodTypes(baseDataValidator); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + private void validateDomainRules(final DataValidatorBuilder baseDataValidator) { + final boolean isMinTermGreaterThanMax = this.accountTermAndPreClosure.depositTermDetail() + .isMinDepositTermGreaterThanMaxDepositTerm(); + // deposit period should be within min and max deposit term + if (isMinTermGreaterThanMax) { + final Integer maxTerm = this.accountTermAndPreClosure.depositTermDetail().maxDepositTerm(); + baseDataValidator.reset().parameter(maxDepositTermParamName).value(maxTerm) + .failWithCodeNoParameterAddedToErrorCode("max.term.lessthan.min.term"); + } + + final Integer depositPeriod = this.accountTermAndPreClosure.depositPeriod(); + if (this.accountTermAndPreClosure.depositTermDetail().maxDepositTerm() != null) { + baseDataValidator.reset().parameter(depositPeriodParamName).value(depositPeriod).notNull(); + } + if (depositPeriod != null) { + final SavingsPeriodFrequencyType depositPeriodFrequencyType = this.accountTermAndPreClosure.depositPeriodFrequencyType(); + final boolean isValidDepositPeriod = this.accountTermAndPreClosure.depositTermDetail() + .isDepositBetweenMinAndMax(depositStartDate(), calculateMaturityDate()); + if (!isValidDepositPeriod) { + baseDataValidator.reset().parameter(depositPeriodParamName).value(depositPeriod) + .failWithCodeNoParameterAddedToErrorCode("deposit.period.not.between.min.and.max.deposit.term"); + } else { + final Integer inMultiplesOf = this.accountTermAndPreClosure.depositTermDetail().inMultiplesOfDepositTerm(); + if (inMultiplesOf != null) { + final boolean isValid = this.accountTermAndPreClosure.depositTermDetail().isValidInMultiplesOfPeriod(depositPeriod, + depositPeriodFrequencyType); + if (!isValid) { + baseDataValidator.reset().parameter(depositPeriodParamName).value(depositPeriod) + .failWithCodeNoParameterAddedToErrorCode("deposit.period.not.multiple.of.term"); + } + } + } + if (isAccountLocked(calculateMaturityDate())) { + baseDataValidator.reset().parameter(depositPeriodParamName).value(depositPeriod).failWithCode( + "deposit.period.must.be.greater.than.lock.in.period", + "Deposit period must be greater than account lock-in period."); + } + + } + if (firstDepositDateBeforeAccountSubmittedOrActivationDate()) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode( + "expected.first.deposit.date.must.be.after.account.submitted.or.activation.date"); + } + + // FIXME: Handle this scenario + /* + * //final boolean recurringFrequencyBeforeDepositPeriod = recurringFrequencyBeforeDepositPeriod(); + * + * if (!recurringFrequencyBeforeDepositPeriod) { + * baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode( + * "recurring.frequency.not.before.deposit.period"); } + */ + } + + public void validateApplicableInterestRate() { + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME); + LocalDate maturityDate = calculateMaturityDate(); + if (this.chart != null) { + final LocalDate chartFromDate = this.chart.getFromDate(); + LocalDate chartEndDate = this.chart.getEndDate(); + chartEndDate = chartEndDate == null ? DateUtils.getBusinessLocalDate() : chartEndDate; + + final LocalDateInterval chartInterval = LocalDateInterval.create(chartFromDate, chartEndDate); + if (!chartInterval.contains(accountSubmittedOrActivationDate())) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("no.valid.interest.rate.slab.available.for.date.range"); + } + + if (maturityDate == null) { + maturityDate = DateUtils.getBusinessLocalDate(); + } + + final BigDecimal maturityAmount = this.accountTermAndPreClosure.depositAmount(); + BigDecimal applicableInterestRate = this.chart.getApplicableInterestRate(maturityAmount, depositStartDate(), maturityDate, + this.client); + + if (applicableInterestRate.compareTo(BigDecimal.ZERO) == 0) { + baseDataValidator.reset() + .failWithCodeNoParameterAddedToErrorCode("no.applicable.interest.rate.is.found.based.on.amount.and.deposit.period"); + } + + } + // CBS: chart is optional — CBS base interest rate chart is used instead + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + /* + * final boolean recurringFrequencyBeforeDepositPeriod = recurringFrequencyBeforeDepositPeriod(); + * + * if (!recurringFrequencyBeforeDepositPeriod) { + * baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode( + * "recurring.frequency.not.before.deposit.period"); } + */ + } + + public boolean isReinvestOnClosure() { + return this.accountTermAndPreClosure.isReinvestOnClosure(); + } + + public boolean isTransferToSavingsOnClosure() { + return this.accountTermAndPreClosure.isTransferToSavingsOnClosure(); + } + + public RecurringDepositAccount reInvest(BigDecimal depositAmount) { + + final DepositAccountTermAndPreClosure newAccountTermAndPreClosure = this.accountTermAndPreClosure.copy(depositAmount); + final DepositAccountRecurringDetail recurringDetail = this.recurringDetail.copy(); + final SavingsProduct product = this.product; + final InterestRateChart productChart = product.applicableChart(getClosedOnDate()); + final DepositAccountInterestRateChart newChart = DepositAccountInterestRateChart.from(productChart); + final String accountNumber = null; + final ExternalId externalId = this.externalId; + final AccountType accountType = AccountType.fromInt(this.accountType); + final SavingsPostingInterestPeriodType postingPeriodType = SavingsPostingInterestPeriodType.fromInt(this.interestPostingPeriodType); + final SavingsCompoundingInterestPeriodType compoundingPeriodType = SavingsCompoundingInterestPeriodType + .fromInt(this.interestCompoundingPeriodType); + final SavingsInterestCalculationType interestCalculationType = SavingsInterestCalculationType.fromInt(this.interestCalculationType); + final SavingsInterestCalculationDaysInYearType daysInYearType = SavingsInterestCalculationDaysInYearType + .fromInt(this.interestCalculationDaysInYearType); + final BigDecimal minRequiredOpeningBalance = depositAmount; + final BigDecimal interestRate = BigDecimal.ZERO; + final Set savingsAccountCharges = null; + final SavingsPeriodFrequencyType lockinPeriodFrequencyType = SavingsPeriodFrequencyType.fromInt(this.lockinPeriodFrequencyType); + final Integer lockinPeriodFrequency = this.lockinPeriodFrequency; + final boolean withdrawalFeeApplicableForTransfer = false; + + LocalDate now = getClosedOnDate(); + newAccountTermAndPreClosure.updateExpectedFirstDepositDate(now); + + RecurringDepositAccount rdAccount = RecurringDepositAccount.createNewActivatedAccount(client, group, product, savingsOfficer, + accountNumber, externalId, accountType, getClosedOnDate(), closedBy, interestRate, compoundingPeriodType, postingPeriodType, + interestCalculationType, daysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType, + withdrawalFeeApplicableForTransfer, savingsAccountCharges, newAccountTermAndPreClosure, recurringDetail, newChart, + withHoldTax); + + rdAccount.setDatesFrom(now); + newAccountTermAndPreClosure.updateAccountReference(rdAccount); + recurringDetail.updateAccountReference(rdAccount); + + return rdAccount; + + } + + private boolean firstDepositDateBeforeAccountSubmittedOrActivationDate() { + final LocalDate expectedFirstDepositLocalDate = accountTermAndPreClosure.getExpectedFirstDepositOnDate(); + return expectedFirstDepositLocalDate != null + && DateUtils.isBefore(expectedFirstDepositLocalDate, accountSubmittedOrActivationDate()); + } + + public void setDatesFrom(final LocalDate now) { + this.rejectedOnDate = null; + this.rejectedBy = null; + this.withdrawnOnDate = null; + this.withdrawnBy = null; + this.closedOnDate = null; + this.closedBy = null; + + this.activatedBy = null; + this.lockedInUntilDate = null; + + this.activatedOnDate = now; + } + + public void setClosedOnDate(final LocalDate closedOnDate) { + this.closedOnDate = closedOnDate; + } + + @Override + protected boolean isTransferInterestToOtherAccount() { + return this.accountTermAndPreClosure.isTransferInterestToLinkedAccount(); + } + + public void generateSchedule(final PeriodFrequencyType frequency, final Integer recurringEvery, final Calendar calendar) { + this.depositScheduleInstallments.clear(); + LocalDate installmentDate = null; + if (this.isCalendarInherited()) { + installmentDate = CalendarUtils.getNextScheduleDate(calendar, accountSubmittedOrActivationDate()); + } else { + installmentDate = depositStartDate(); + } + + int installmentNumber = 1; + final LocalDate maturityDate = calcualteScheduleTillDate(frequency, recurringEvery); + final BigDecimal depositAmount = this.recurringDetail.mandatoryRecommendedDepositAmount(); + while (DateUtils.isBefore(installmentDate, maturityDate)) { + final RecurringDepositScheduleInstallment installment = RecurringDepositScheduleInstallment.installment(this, installmentNumber, + installmentDate, depositAmount); + addDepositScheduleInstallment(installment); + installmentDate = DepositAccountUtils.calculateNextDepositDate(installmentDate, frequency, recurringEvery); + installmentNumber += 1; + } + updateDepositAmount(); + } + + private LocalDate calcualteScheduleTillDate(final PeriodFrequencyType frequency, final Integer recurringEvery) { + LocalDate tillDate = calculateMaturityDate(); + if (tillDate == null) { + final LocalDate today = DateUtils.getBusinessLocalDate(); + tillDate = DepositAccountUtils.calculateNextDepositDate(today, frequency, + recurringEvery * (DepositAccountUtils.GENERATE_MINIMUM_NUMBER_OF_FUTURE_INSTALMENTS + 1)); + } + return tillDate; + } + + private List depositScheduleInstallments() { + return this.depositScheduleInstallments; + } + + private void addDepositScheduleInstallment(final RecurringDepositScheduleInstallment installment) { + this.depositScheduleInstallments.add(installment); + } + + public boolean isCalendarInherited() { + return this.recurringDetail.isCalendarInherited(); + } + + public void updateOverduePayments(final LocalDate todayDate) { + LocalDate overdueUptoDate = this.maturityDate(); + if (overdueUptoDate == null || DateUtils.isAfter(overdueUptoDate, todayDate)) { + overdueUptoDate = todayDate; + } + + final List installments = depositScheduleInstallments(); + int noOfOverdueInstallments = 0; + Money totalOverdueAmount = Money.zero(getCurrency()); + for (RecurringDepositScheduleInstallment installment : installments) { + if (installment.isNotFullyPaidOff() && DateUtils.isAfter(overdueUptoDate, installment.dueDate())) { + noOfOverdueInstallments++; + totalOverdueAmount = totalOverdueAmount.plus(installment.getDepositAmountOutstanding(getCurrency())); + } + } + this.recurringDetail.updateOverdueDetails(noOfOverdueInstallments, totalOverdueAmount); + } + + @Override + public boolean allowWithdrawal() { + return this.recurringDetail.allowWithdrawal(); + } + + public boolean adjustAdvanceTowardsFuturePayments() { + return this.recurringDetail.adjustAdvanceTowardsFuturePayments(); + } + + @Override + public boolean isTransactionsAllowed() { + return isActive() || isAccountMatured(); + } + + public DepositAccountRecurringDetail getRecurringDetail() { + return this.recurringDetail; + } + + static class RecurringDepositScheduleInstallmentComparator implements Comparator { + + @Override + public int compare(final RecurringDepositScheduleInstallment o1, final RecurringDepositScheduleInstallment o2) { + return o1.installmentNumber().compareTo(o2.installmentNumber()); + } + } + + @Override + public void loadLazyCollections() { + this.depositScheduleInstallments.size(); + super.loadLazyCollections(); + this.chart.getId(); + } + + public BigDecimal getDepositAmount() { + return this.accountTermAndPreClosure.depositAmount(); + } + + @Override + public DepositAccountType depositAccountType() { + return DepositAccountType.fromInt(300); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountAssembler.java index 2d72f6d59..3199914b7 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountAssembler.java @@ -1,623 +1,641 @@ -/** - * 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.accountFacilityConfigParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.accountNoParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.accountStatusParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.guardianClientIdParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.modeOfOperationParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.accountTypeParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.customerSegmentParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.guardianCifNumberParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.guardianNameParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.guardianRelationshipParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.introducerCifNumberParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.introducerIdParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.introducerNameParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.jointHoldersParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.linkedAccountIdParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.linkedAccountNoParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.linkedAccountTypeParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.minorDateOfBirthParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.nomineeParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.specialCategoryParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.weakerSectionParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.nominationFacilityParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.autoDebitFacilityParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.allowOverdraftParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.clientIdParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.enforceMinRequiredBalanceParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.externalIdParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.fieldOfficerIdParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.groupIdParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.interestCalculationDaysInYearTypeParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.interestCalculationTypeParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.interestCompoundingPeriodTypeParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.interestPostingPeriodTypeParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.lienAllowedParamName; -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.maxAllowedLienLimitParamName; -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.minRequiredOpeningBalanceParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.nominalAnnualInterestRateOverdraftParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.nominalAnnualInterestRateParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.overdraftLimitParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.productIdParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.submittedOnDateParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.withHoldTaxParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.withdrawalFeeForTransfersParamName; - -import com.google.gson.Gson; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import java.math.BigDecimal; -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Set; -import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; -import org.apache.fineract.infrastructure.core.api.JsonCommand; -import org.apache.fineract.infrastructure.core.exception.UnsupportedParameterException; -import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; -import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; -import org.apache.fineract.organisation.staff.domain.Staff; -import org.apache.fineract.organisation.staff.domain.StaffRepositoryWrapper; -import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService; -import org.apache.fineract.portfolio.accountdetails.domain.AccountType; -import org.apache.fineract.portfolio.client.domain.Client; -import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper; -import org.apache.fineract.portfolio.client.exception.ClientNotActiveException; -import org.apache.fineract.portfolio.group.domain.Group; -import org.apache.fineract.portfolio.group.domain.GroupRepositoryWrapper; -import org.apache.fineract.portfolio.group.exception.CenterNotActiveException; -import org.apache.fineract.portfolio.group.exception.ClientNotInGroupException; -import org.apache.fineract.portfolio.group.exception.GroupNotActiveException; -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.SavingsAccountData; -import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData; -import org.apache.fineract.portfolio.savings.exception.SavingsProductNotFoundException; -import org.apache.fineract.useradministration.domain.AppUser; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.stereotype.Service; - -@Service -public class SavingsAccountAssembler { - - private static final Logger LOG = LoggerFactory.getLogger(SavingsAccountAssembler.class); - private final SavingsAccountTransactionSummaryWrapper savingsAccountTransactionSummaryWrapper; - private final SavingsAccountTransactionDataSummaryWrapper savingsAccountTransactionDataSummaryWrapper; - private final SavingsHelper savingsHelper; - private final ClientRepositoryWrapper clientRepository; - private final GroupRepositoryWrapper groupRepository; - private final StaffRepositoryWrapper staffRepository; - private final SavingsProductRepository savingProductRepository; - private final SavingsAccountRepositoryWrapper savingsAccountRepository; - private final SavingsAccountChargeAssembler savingsAccountChargeAssembler; - private final FromJsonHelper fromApiJsonHelper; - private final JdbcTemplate jdbcTemplate; - private final ConfigurationDomainService configurationDomainService; - private final ExternalIdFactory externalIdFactory; - - @Autowired - public SavingsAccountAssembler(final SavingsAccountTransactionSummaryWrapper savingsAccountTransactionSummaryWrapper, - final SavingsAccountTransactionDataSummaryWrapper savingsAccountTransactionDataSummaryWrapper, - final ClientRepositoryWrapper clientRepository, final GroupRepositoryWrapper groupRepository, - final StaffRepositoryWrapper staffRepository, final SavingsProductRepository savingProductRepository, - final SavingsAccountRepositoryWrapper savingsAccountRepository, - final SavingsAccountChargeAssembler savingsAccountChargeAssembler, final FromJsonHelper fromApiJsonHelper, - final AccountTransfersReadPlatformService accountTransfersReadPlatformService, final JdbcTemplate jdbcTemplate, - final ConfigurationDomainService configurationDomainService, ExternalIdFactory externalIdFactory) { - this.savingsAccountTransactionSummaryWrapper = savingsAccountTransactionSummaryWrapper; - this.savingsAccountTransactionDataSummaryWrapper = savingsAccountTransactionDataSummaryWrapper; - this.clientRepository = clientRepository; - this.groupRepository = groupRepository; - this.staffRepository = staffRepository; - this.savingProductRepository = savingProductRepository; - this.savingsAccountRepository = savingsAccountRepository; - this.savingsAccountChargeAssembler = savingsAccountChargeAssembler; - this.fromApiJsonHelper = fromApiJsonHelper; - savingsHelper = new SavingsHelper(accountTransfersReadPlatformService); - this.jdbcTemplate = jdbcTemplate; - this.configurationDomainService = configurationDomainService; - this.externalIdFactory = externalIdFactory; - } - - /** - * Assembles a new {@link SavingsAccount} from JSON details passed in request inheriting details where relevant from - * chosen {@link SavingsProduct}. - */ - public SavingsAccount assembleFrom(final JsonCommand command, final AppUser submittedBy) { - - final JsonElement element = command.parsedJson(); - - final String accountNo = this.fromApiJsonHelper.extractStringNamed(accountNoParamName, element); - final String externalId = this.fromApiJsonHelper.extractStringNamed(externalIdParamName, element); - final Long productId = this.fromApiJsonHelper.extractLongNamed(productIdParamName, element); - - final SavingsProduct product = this.savingProductRepository.findById(productId) - .orElseThrow(() -> new SavingsProductNotFoundException(productId)); - - Client client = null; - Group group = null; - Staff fieldOfficer = null; - AccountType accountType = AccountType.INVALID; - - final Long clientId = this.fromApiJsonHelper.extractLongNamed(clientIdParamName, element); - if (clientId != null) { - client = this.clientRepository.findOneWithNotFoundDetection(clientId); - accountType = AccountType.INDIVIDUAL; - if (client.isNotActive()) { - throw new ClientNotActiveException(clientId); - } - } - - final Long groupId = this.fromApiJsonHelper.extractLongNamed(groupIdParamName, element); - if (groupId != null) { - group = this.groupRepository.findOneWithNotFoundDetection(groupId); - accountType = AccountType.GROUP; - if (group.isNotActive()) { - if (group.isCenter()) { - throw new CenterNotActiveException(groupId); - } - throw new GroupNotActiveException(groupId); - } - } - - if (group != null && client != null) { - if (!group.hasClientAsMember(client)) { - throw new ClientNotInGroupException(clientId, groupId); - } - accountType = AccountType.JLG; - } - - if ((Boolean) command.booleanPrimitiveValueOfParameterNamed("isGSIM") != null) { - if (command.booleanPrimitiveValueOfParameterNamed("isGSIM")) { - accountType = AccountType.GSIM; - } - } - - final Long fieldOfficerId = this.fromApiJsonHelper.extractLongNamed(fieldOfficerIdParamName, element); - if (fieldOfficerId != null) { - fieldOfficer = this.staffRepository.findOneWithNotFoundDetection(fieldOfficerId); - } - - final LocalDate submittedOnDate = this.fromApiJsonHelper.extractLocalDateNamed(submittedOnDateParamName, element); - - BigDecimal interestRate = null; - if (command.parameterExists(nominalAnnualInterestRateParamName)) { - interestRate = command.bigDecimalValueOfParameterNamed(nominalAnnualInterestRateParamName); - } else { - interestRate = product.nominalAnnualInterestRate(); - } - - SavingsCompoundingInterestPeriodType interestCompoundingPeriodType = null; - final Integer interestPeriodTypeValue = command.integerValueOfParameterNamed(interestCompoundingPeriodTypeParamName); - if (interestPeriodTypeValue != null) { - interestCompoundingPeriodType = SavingsCompoundingInterestPeriodType.fromInt(interestPeriodTypeValue); - } else { - interestCompoundingPeriodType = product.interestCompoundingPeriodType(); - } - - SavingsPostingInterestPeriodType interestPostingPeriodType = null; - final Integer interestPostingPeriodTypeValue = command.integerValueOfParameterNamed(interestPostingPeriodTypeParamName); - if (interestPostingPeriodTypeValue != null) { - interestPostingPeriodType = SavingsPostingInterestPeriodType.fromInt(interestPostingPeriodTypeValue); - } else { - interestPostingPeriodType = product.interestPostingPeriodType(); - } - - SavingsInterestCalculationType interestCalculationType = null; - final Integer interestCalculationTypeValue = command.integerValueOfParameterNamed(interestCalculationTypeParamName); - if (interestCalculationTypeValue != null) { - interestCalculationType = SavingsInterestCalculationType.fromInt(interestCalculationTypeValue); - } else { - interestCalculationType = product.interestCalculationType(); - } - - SavingsInterestCalculationDaysInYearType interestCalculationDaysInYearType = null; - final Integer interestCalculationDaysInYearTypeValue = command - .integerValueOfParameterNamed(interestCalculationDaysInYearTypeParamName); - if (interestCalculationDaysInYearTypeValue != null) { - interestCalculationDaysInYearType = SavingsInterestCalculationDaysInYearType.fromInt(interestCalculationDaysInYearTypeValue); - } else { - interestCalculationDaysInYearType = product.interestCalculationDaysInYearType(); - } - - BigDecimal minRequiredOpeningBalance = null; - if (command.parameterExists(minRequiredOpeningBalanceParamName)) { - minRequiredOpeningBalance = command.bigDecimalValueOfParameterNamed(minRequiredOpeningBalanceParamName); - } else { - minRequiredOpeningBalance = product.minRequiredOpeningBalance(); - } - - Integer lockinPeriodFrequency = null; - if (command.parameterExists(lockinPeriodFrequencyParamName)) { - lockinPeriodFrequency = command.integerValueOfParameterNamed(lockinPeriodFrequencyParamName); - } else { - lockinPeriodFrequency = product.lockinPeriodFrequency(); - } - - SavingsPeriodFrequencyType lockinPeriodFrequencyType = null; - Integer lockinPeriodFrequencyTypeValue = null; - if (command.parameterExists(lockinPeriodFrequencyTypeParamName)) { - lockinPeriodFrequencyTypeValue = command.integerValueOfParameterNamed(lockinPeriodFrequencyTypeParamName); - if (lockinPeriodFrequencyTypeValue != null) { - lockinPeriodFrequencyType = SavingsPeriodFrequencyType.fromInt(lockinPeriodFrequencyTypeValue); - } - } else { - lockinPeriodFrequencyType = product.lockinPeriodFrequencyType(); - } - boolean iswithdrawalFeeApplicableForTransfer = false; - if (command.parameterExists(withdrawalFeeForTransfersParamName)) { - iswithdrawalFeeApplicableForTransfer = command.booleanPrimitiveValueOfParameterNamed(withdrawalFeeForTransfersParamName); - } - - final Set charges = this.savingsAccountChargeAssembler.fromParsedJson(element, product.currency().getCode()); - - boolean allowOverdraft = false; - if (command.parameterExists(allowOverdraftParamName)) { - allowOverdraft = command.booleanPrimitiveValueOfParameterNamed(allowOverdraftParamName); - } else { - allowOverdraft = product.isAllowOverdraft(); - } - - BigDecimal overdraftLimit = BigDecimal.ZERO; - if (command.parameterExists(overdraftLimitParamName)) { - overdraftLimit = command.bigDecimalValueOfParameterNamedDefaultToNullIfZero(overdraftLimitParamName); - } else { - overdraftLimit = product.overdraftLimit(); - } - - BigDecimal nominalAnnualInterestRateOverdraft = BigDecimal.ZERO; - if (command.parameterExists(nominalAnnualInterestRateOverdraftParamName)) { - nominalAnnualInterestRateOverdraft = command - .bigDecimalValueOfParameterNamedDefaultToNullIfZero(nominalAnnualInterestRateOverdraftParamName); - } else { - nominalAnnualInterestRateOverdraft = product.nominalAnnualInterestRateOverdraft(); - } - - BigDecimal minOverdraftForInterestCalculation = BigDecimal.ZERO; - if (command.parameterExists(minOverdraftForInterestCalculationParamName)) { - minOverdraftForInterestCalculation = command - .bigDecimalValueOfParameterNamedDefaultToNullIfZero(minOverdraftForInterestCalculationParamName); - } else { - minOverdraftForInterestCalculation = product.minOverdraftForInterestCalculation(); - } - - boolean enforceMinRequiredBalance = false; - if (command.parameterExists(enforceMinRequiredBalanceParamName)) { - enforceMinRequiredBalance = command.booleanPrimitiveValueOfParameterNamed(enforceMinRequiredBalanceParamName); - } else { - enforceMinRequiredBalance = product.isMinRequiredBalanceEnforced(); - } - - BigDecimal minRequiredBalance = BigDecimal.ZERO; - if (command.parameterExists(minRequiredBalanceParamName)) { - minRequiredBalance = command.bigDecimalValueOfParameterNamedDefaultToNullIfZero(minRequiredBalanceParamName); - } else { - minRequiredBalance = product.minRequiredBalance(); - } - - boolean lienAllowed = false; - if (command.parameterExists(lienAllowedParamName)) { - lienAllowed = command.booleanPrimitiveValueOfParameterNamed(lienAllowedParamName); - } else { - lienAllowed = product.isLienAllowed(); - } - - BigDecimal maxAllowedLienLimit = BigDecimal.ZERO; - if (command.parameterExists(maxAllowedLienLimitParamName)) { - maxAllowedLienLimit = command.bigDecimalValueOfParameterNamedDefaultToNullIfZero(maxAllowedLienLimitParamName); - } else { - maxAllowedLienLimit = product.maxAllowedLienLimit(); - } - - boolean withHoldTax = product.withHoldTax(); - if (command.parameterExists(withHoldTaxParamName)) { - withHoldTax = command.booleanPrimitiveValueOfParameterNamed(withHoldTaxParamName); - if (withHoldTax && product.getTaxGroup() == null) { - throw new UnsupportedParameterException(Arrays.asList(withHoldTaxParamName)); - } - } - - final SavingsAccount account = SavingsAccount.createNewApplicationForSubmittal(client, group, product, fieldOfficer, accountNo, - externalIdFactory.create(externalId), accountType, submittedOnDate, submittedBy, interestRate, - interestCompoundingPeriodType, interestPostingPeriodType, interestCalculationType, interestCalculationDaysInYearType, - minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType, iswithdrawalFeeApplicableForTransfer, charges, - allowOverdraft, overdraftLimit, enforceMinRequiredBalance, minRequiredBalance, maxAllowedLienLimit, lienAllowed, - nominalAnnualInterestRateOverdraft, minOverdraftForInterestCalculation, withHoldTax); - account.setHelpers(this.savingsAccountTransactionSummaryWrapper, this.savingsHelper); - - account.validateNewApplicationState(SAVINGS_ACCOUNT_RESOURCE_NAME); - - account.validateAccountValuesWithProduct(); - - // CBS extended fields - applyCbsFields(command, element, account); - - return account; - } - - private static final Gson GSON = new Gson(); - - public void applyCbsFields(final JsonCommand command, final JsonElement element, final SavingsAccount account) { - // Product classification - final String cbsAccountType = this.fromApiJsonHelper.extractStringNamed(accountTypeParamName, element); - if (cbsAccountType != null) account.setCbsAccountType(cbsAccountType); - - final String customerSegment = this.fromApiJsonHelper.extractStringNamed(customerSegmentParamName, element); - if (customerSegment != null) account.setCustomerSegment(customerSegment); - - // Status of Account - final String accountStatus = this.fromApiJsonHelper.extractStringNamed(accountStatusParamName, element); - if (accountStatus != null) account.setAccountStatus(accountStatus); - - final String modeOfOperation = this.fromApiJsonHelper.extractStringNamed(modeOfOperationParamName, element); - if (modeOfOperation != null) account.setModeOfOperation(modeOfOperation); - - account.setSpecialCategory(this.fromApiJsonHelper.extractStringNamed(specialCategoryParamName, element)); - account.setWeakerSection(this.fromApiJsonHelper.extractStringNamed(weakerSectionParamName, element)); - - if (command.parameterExists(nominationFacilityParamName)) { - account.setNominationFacility(this.fromApiJsonHelper.extractBooleanNamed(nominationFacilityParamName, element)); - } - if (command.parameterExists(autoDebitFacilityParamName)) { - account.setAutoDebitFacility(this.fromApiJsonHelper.extractBooleanNamed(autoDebitFacilityParamName, element)); - } - - account.setGuardianName(this.fromApiJsonHelper.extractStringNamed(guardianNameParamName, element)); - account.setGuardianCifNumber(this.fromApiJsonHelper.extractStringNamed(guardianCifNumberParamName, element)); - account.setGuardianRelationship(this.fromApiJsonHelper.extractStringNamed(guardianRelationshipParamName, element)); - - if (command.parameterExists(minorDateOfBirthParamName)) { - final LocalDate minorDob = this.fromApiJsonHelper.extractLocalDateNamed(minorDateOfBirthParamName, element); - account.setMinorDateOfBirth(minorDob); - } - - final Long guardianClientId = this.fromApiJsonHelper.extractLongNamed(guardianClientIdParamName, element); - if (guardianClientId != null) account.setGuardianClientId(guardianClientId); - - // Introducer - final Long introducerId = this.fromApiJsonHelper.extractLongNamed(introducerIdParamName, element); - if (introducerId != null) account.setIntroducerClientId(introducerId); - account.setIntroducerName(this.fromApiJsonHelper.extractStringNamed(introducerNameParamName, element)); - account.setIntroducerCifNumber(this.fromApiJsonHelper.extractStringNamed(introducerCifNumberParamName, element)); - - // Nominee (stored as JSON string) - if (element.getAsJsonObject().has(nomineeParamName)) { - final JsonElement nomineeEl = element.getAsJsonObject().get(nomineeParamName); - if (nomineeEl != null && !nomineeEl.isJsonNull()) { - account.setNomineeData(nomineeEl.toString()); - } - } - - // Deposit Account Link - final Long linkedAccountId = this.fromApiJsonHelper.extractLongNamed(linkedAccountIdParamName, element); - if (linkedAccountId != null) account.setLinkedAccountId(linkedAccountId); - account.setLinkedAccountType(this.fromApiJsonHelper.extractStringNamed(linkedAccountTypeParamName, element)); - account.setLinkedAccountNo(this.fromApiJsonHelper.extractStringNamed(linkedAccountNoParamName, element)); - - // Account Facility Config - if (element.getAsJsonObject().has(accountFacilityConfigParamName)) { - final JsonElement configEl = element.getAsJsonObject().get(accountFacilityConfigParamName); - if (configEl != null && !configEl.isJsonNull() && configEl.isJsonObject()) { - SavingsAccountFacilityConfig facilityConfig = account.getFacilityConfig(); - if (facilityConfig == null) { - facilityConfig = new SavingsAccountFacilityConfig(account); - account.setFacilityConfig(facilityConfig); - } - facilityConfig.updateFrom(configEl.getAsJsonObject()); - } - } - - // Joint holders - if (element.getAsJsonObject().has(jointHoldersParamName)) { - final JsonElement holdersEl = element.getAsJsonObject().get(jointHoldersParamName); - if (holdersEl != null && !holdersEl.isJsonNull() && holdersEl.isJsonArray()) { - final com.google.gson.JsonArray holdersArray = holdersEl.getAsJsonArray(); - final List holders = new ArrayList<>(); - for (int i = 0; i < holdersArray.size(); i++) { - final JsonObject h = holdersArray.get(i).getAsJsonObject(); - final Long clientId = h.has("clientId") && !h.get("clientId").isJsonNull() - ? h.get("clientId").getAsLong() : null; - final String cifNumber = h.has("cifNumber") && !h.get("cifNumber").isJsonNull() - ? h.get("cifNumber").getAsString() : null; - final String displayName = h.has("displayName") && !h.get("displayName").isJsonNull() - ? h.get("displayName").getAsString() : null; - final BigDecimal sharePercentage = h.has("sharePercentage") && !h.get("sharePercentage").isJsonNull() - ? h.get("sharePercentage").getAsBigDecimal() : null; - final boolean isPrimary = h.has("isPrimary") && h.get("isPrimary").getAsBoolean(); - String signingAuthority = h.has("signingAuthority") && !h.get("signingAuthority").isJsonNull() - ? h.get("signingAuthority").getAsString() : null; - String role = h.has("role") && !h.get("role").isJsonNull() - ? h.get("role").getAsString() : null; - - if (signingAuthority == null || signingAuthority.trim().isEmpty()) { - signingAuthority = isPrimary ? "Mandatory" : "Optional"; - } - if (role == null || role.trim().isEmpty()) { - role = isPrimary ? "Primary Account Holder" : "Co-Holder"; - } - - final boolean isMandatory = h.has("isMandatory") && h.get("isMandatory").getAsBoolean(); - holders.add(new SavingsAccountJointHolder(account, clientId, cifNumber, displayName, - sharePercentage, signingAuthority, role, isPrimary, isMandatory)); - } - account.getJointHolders().clear(); - account.getJointHolders().addAll(holders); - } - } - } - - public SavingsAccount assembleFrom(final Long savingsId, final boolean backdatedTxnsAllowedTill) { - SavingsAccount account = this.savingsAccountRepository.findSavingsWithNotFoundDetection(savingsId, backdatedTxnsAllowedTill); - return loadTransactionsToSavingsAccount(account, backdatedTxnsAllowedTill); - } - - public SavingsAccount loadTransactionsToSavingsAccount(final SavingsAccount account, final boolean backdatedTxnsAllowedTill) { - List savingsAccountTransactions = null; - if (backdatedTxnsAllowedTill) { - LocalDate pivotDate = account.getSummary().getInterestPostedTillDate(); - boolean isNotPresent = pivotDate == null; - if (!isNotPresent) { - // Get savings account transactions - if (isRelaxingDaysConfigForPivotDateEnabled()) { - final Long relaxingDaysForPivotDate = this.configurationDomainService.retrieveRelaxingDaysConfigForPivotDate(); - LocalDate interestPostedTillDate = account.getSummary().getInterestPostedTillDate(); - savingsAccountTransactions = this.savingsAccountRepository.findTransactionsAfterPivotDate(account, - interestPostedTillDate.minusDays(relaxingDaysForPivotDate)); - - savingsAccountTransactions.get(0).getSavingsAccount() - .setStartInterestCalculationDate(interestPostedTillDate.minusDays(relaxingDaysForPivotDate)); - List pivotDateTransaction = this.savingsAccountRepository - .findTransactionRunningBalanceBeforePivotDate(account, - interestPostedTillDate.minusDays(relaxingDaysForPivotDate + 1)); - if (pivotDateTransaction != null && !pivotDateTransaction.isEmpty()) { - account.getSummary().setRunningBalanceOnPivotDate(pivotDateTransaction.get(pivotDateTransaction.size() - 1) - .getRunningBalance(account.getCurrency()).getAmount()); - } - } else { - savingsAccountTransactions = this.savingsAccountRepository.findTransactionsAfterPivotDate(account, - account.getSummary().getInterestPostedTillDate()); - } - - if (savingsAccountTransactions != null && savingsAccountTransactions.size() > 0) { - // Update transient variable - account.setSavingsAccountTransactions(savingsAccountTransactions); - } - } else { - savingsAccountTransactions = this.savingsAccountRepository.findAllTransactions(account); - account.setSavingsAccountTransactions(savingsAccountTransactions); - } - } - - account.setHelpers(this.savingsAccountTransactionSummaryWrapper, this.savingsHelper); - return account; - } - - public SavingsAccountData assembleSavings(final SavingsAccountData account) { - - // Update last running balance on account level - final boolean backdatedTxnsAllowedTill = this.configurationDomainService.retrievePivotDateConfig(); - if (backdatedTxnsAllowedTill && account.getSavingsAccountTransactionData() != null - && account.getSummary().getInterestPostedTillDate() != null) { - List removalList = new ArrayList<>(); - - for (int i = 0; i < account.getSavingsAccountTransactionData().size(); i++) { - SavingsAccountTransactionData savingsAccountTransaction = account.getSavingsAccountTransactionData().get(i); - removalList.add(savingsAccountTransaction); - if ((savingsAccountTransaction.isInterestPostingAndNotReversed() - || savingsAccountTransaction.isOverdraftInterestAndNotReversed()) - && !savingsAccountTransaction.isReversalTransaction()) { - account.getSummary().setRunningBalanceOnPivotDate(savingsAccountTransaction.getRunningBalance()); - account.setLastSavingsAccountTransaction(savingsAccountTransaction); - break; - } - } - account.getSavingsAccountTransactionData().removeAll(removalList); - } else { - account.getSummary().setRunningBalanceOnPivotDate(BigDecimal.ZERO); - } - account.setHelpers(this.savingsAccountTransactionDataSummaryWrapper, this.savingsHelper); - return account; - } - - public boolean getPivotConfigStatus() { - return this.configurationDomainService.retrievePivotDateConfig(); - } - - public boolean isRelaxingDaysConfigForPivotDateEnabled() { - return this.configurationDomainService.isRelaxingDaysConfigForPivotDateEnabled(); - } - - public void setHelpers(final SavingsAccount account) { - account.setHelpers(this.savingsAccountTransactionSummaryWrapper, this.savingsHelper); - } - - /** - * Assembles a new {@link SavingsAccount} from JSON details passed in request inheriting details where relevant from - * chosen {@link SavingsProduct}. - */ - public SavingsAccount assembleFrom(final Client client, final Group group, final Long productId, final LocalDate appliedonDate, - final AppUser appliedBy) { - - AccountType accountType = AccountType.INVALID; - if (client != null) { - accountType = AccountType.INDIVIDUAL; - if (client.isNotActive()) { - throw new ClientNotActiveException(client.getId()); - } - } - - if (group != null) { - accountType = AccountType.GROUP; - if (group.isNotActive()) { - if (group.isCenter()) { - throw new CenterNotActiveException(group.getId()); - } - throw new GroupNotActiveException(group.getId()); - } - } - - if (group != null && client != null) { - if (!group.hasClientAsMember(client)) { - throw new ClientNotInGroupException(client.getId(), group.getId()); - } - accountType = AccountType.JLG; - } - final SavingsProduct product = this.savingProductRepository.findById(productId).orElseThrow(); - final Set charges = this.savingsAccountChargeAssembler.fromSavingsProduct(product); - final SavingsAccount account = SavingsAccount.createNewApplicationForSubmittal(client, group, product, null, null, null, - accountType, appliedonDate, appliedBy, product.nominalAnnualInterestRate(), product.interestCompoundingPeriodType(), - product.interestPostingPeriodType(), product.interestCalculationType(), product.interestCalculationDaysInYearType(), - product.minRequiredOpeningBalance(), product.lockinPeriodFrequency(), product.lockinPeriodFrequencyType(), - product.isWithdrawalFeeApplicableForTransfer(), charges, product.isAllowOverdraft(), product.overdraftLimit(), - product.isMinRequiredBalanceEnforced(), product.minRequiredBalance(), product.maxAllowedLienLimit(), - product.isLienAllowed(), product.nominalAnnualInterestRateOverdraft(), product.minOverdraftForInterestCalculation(), - product.withHoldTax()); - account.setHelpers(this.savingsAccountTransactionSummaryWrapper, this.savingsHelper); - - account.validateNewApplicationState(SAVINGS_ACCOUNT_RESOURCE_NAME); - - account.validateAccountValuesWithProduct(); - - return account; - } - - public void assignSavingAccountHelpers(final SavingsAccount savingsAccount) { - savingsAccount.setHelpers(this.savingsAccountTransactionSummaryWrapper, this.savingsHelper); - } - - public void assignSavingAccountHelpers(final SavingsAccountData savingsAccountData) { - savingsAccountData.setHelpers(this.savingsAccountTransactionDataSummaryWrapper, this.savingsHelper); - } -} +/** + * 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.accountFacilityConfigParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.accountNoParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.accountStatusParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.guardianClientIdParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.modeOfOperationParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.accountTypeParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.customerSegmentParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.guardianCifNumberParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.guardianNameParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.guardianRelationshipParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.introducerCifNumberParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.introducerIdParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.introducerNameParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.jointHoldersParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.linkedAccountIdParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.linkedAccountNoParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.linkedAccountTypeParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.minorDateOfBirthParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.nomineeParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.specialCategoryParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.weakerSectionParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.nominationFacilityParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.autoDebitFacilityParamName; +import static org.apache.fineract.portfolio.savings.DepositsApiConstants.isSeniorCitizenParamName; +import static org.apache.fineract.portfolio.savings.DepositsApiConstants.seniorCitizenInterestRateParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.allowOverdraftParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.clientIdParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.enforceMinRequiredBalanceParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.externalIdParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.fieldOfficerIdParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.groupIdParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.interestCalculationDaysInYearTypeParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.interestCalculationTypeParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.interestCompoundingPeriodTypeParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.interestPostingPeriodTypeParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.lienAllowedParamName; +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.maxAllowedLienLimitParamName; +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.minRequiredOpeningBalanceParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.nominalAnnualInterestRateOverdraftParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.nominalAnnualInterestRateParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.overdraftLimitParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.productIdParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.submittedOnDateParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.withHoldTaxParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.withdrawalFeeForTransfersParamName; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.exception.UnsupportedParameterException; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; +import org.apache.fineract.organisation.staff.domain.Staff; +import org.apache.fineract.organisation.staff.domain.StaffRepositoryWrapper; +import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService; +import org.apache.fineract.portfolio.accountdetails.domain.AccountType; +import org.apache.fineract.portfolio.client.domain.Client; +import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper; +import org.apache.fineract.portfolio.client.exception.ClientNotActiveException; +import org.apache.fineract.portfolio.group.domain.Group; +import org.apache.fineract.portfolio.group.domain.GroupRepositoryWrapper; +import org.apache.fineract.portfolio.group.exception.CenterNotActiveException; +import org.apache.fineract.portfolio.group.exception.ClientNotInGroupException; +import org.apache.fineract.portfolio.group.exception.GroupNotActiveException; +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.SavingsAccountData; +import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData; +import org.apache.fineract.portfolio.savings.exception.SavingsProductNotFoundException; +import org.apache.fineract.useradministration.domain.AppUser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Service; + +@Service +public class SavingsAccountAssembler { + + private static final Logger LOG = LoggerFactory.getLogger(SavingsAccountAssembler.class); + private final SavingsAccountTransactionSummaryWrapper savingsAccountTransactionSummaryWrapper; + private final SavingsAccountTransactionDataSummaryWrapper savingsAccountTransactionDataSummaryWrapper; + private final SavingsHelper savingsHelper; + private final ClientRepositoryWrapper clientRepository; + private final GroupRepositoryWrapper groupRepository; + private final StaffRepositoryWrapper staffRepository; + private final SavingsProductRepository savingProductRepository; + private final SavingsAccountRepositoryWrapper savingsAccountRepository; + private final SavingsAccountChargeAssembler savingsAccountChargeAssembler; + private final FromJsonHelper fromApiJsonHelper; + private final JdbcTemplate jdbcTemplate; + private final ConfigurationDomainService configurationDomainService; + private final ExternalIdFactory externalIdFactory; + + @Autowired + public SavingsAccountAssembler(final SavingsAccountTransactionSummaryWrapper savingsAccountTransactionSummaryWrapper, + final SavingsAccountTransactionDataSummaryWrapper savingsAccountTransactionDataSummaryWrapper, + final ClientRepositoryWrapper clientRepository, final GroupRepositoryWrapper groupRepository, + final StaffRepositoryWrapper staffRepository, final SavingsProductRepository savingProductRepository, + final SavingsAccountRepositoryWrapper savingsAccountRepository, + final SavingsAccountChargeAssembler savingsAccountChargeAssembler, final FromJsonHelper fromApiJsonHelper, + final AccountTransfersReadPlatformService accountTransfersReadPlatformService, final JdbcTemplate jdbcTemplate, + final ConfigurationDomainService configurationDomainService, ExternalIdFactory externalIdFactory) { + this.savingsAccountTransactionSummaryWrapper = savingsAccountTransactionSummaryWrapper; + this.savingsAccountTransactionDataSummaryWrapper = savingsAccountTransactionDataSummaryWrapper; + this.clientRepository = clientRepository; + this.groupRepository = groupRepository; + this.staffRepository = staffRepository; + this.savingProductRepository = savingProductRepository; + this.savingsAccountRepository = savingsAccountRepository; + this.savingsAccountChargeAssembler = savingsAccountChargeAssembler; + this.fromApiJsonHelper = fromApiJsonHelper; + savingsHelper = new SavingsHelper(accountTransfersReadPlatformService); + this.jdbcTemplate = jdbcTemplate; + this.configurationDomainService = configurationDomainService; + this.externalIdFactory = externalIdFactory; + } + + /** + * Assembles a new {@link SavingsAccount} from JSON details passed in request inheriting details where relevant from + * chosen {@link SavingsProduct}. + */ + public SavingsAccount assembleFrom(final JsonCommand command, final AppUser submittedBy) { + + final JsonElement element = command.parsedJson(); + + final String accountNo = this.fromApiJsonHelper.extractStringNamed(accountNoParamName, element); + final String externalId = this.fromApiJsonHelper.extractStringNamed(externalIdParamName, element); + final Long productId = this.fromApiJsonHelper.extractLongNamed(productIdParamName, element); + + final SavingsProduct product = this.savingProductRepository.findById(productId) + .orElseThrow(() -> new SavingsProductNotFoundException(productId)); + + Client client = null; + Group group = null; + Staff fieldOfficer = null; + AccountType accountType = AccountType.INVALID; + + final Long clientId = this.fromApiJsonHelper.extractLongNamed(clientIdParamName, element); + if (clientId != null) { + client = this.clientRepository.findOneWithNotFoundDetection(clientId); + accountType = AccountType.INDIVIDUAL; + if (client.isNotActive()) { + throw new ClientNotActiveException(clientId); + } + } + + final Long groupId = this.fromApiJsonHelper.extractLongNamed(groupIdParamName, element); + if (groupId != null) { + group = this.groupRepository.findOneWithNotFoundDetection(groupId); + accountType = AccountType.GROUP; + if (group.isNotActive()) { + if (group.isCenter()) { + throw new CenterNotActiveException(groupId); + } + throw new GroupNotActiveException(groupId); + } + } + + if (group != null && client != null) { + if (!group.hasClientAsMember(client)) { + throw new ClientNotInGroupException(clientId, groupId); + } + accountType = AccountType.JLG; + } + + if ((Boolean) command.booleanPrimitiveValueOfParameterNamed("isGSIM") != null) { + if (command.booleanPrimitiveValueOfParameterNamed("isGSIM")) { + accountType = AccountType.GSIM; + } + } + + final Long fieldOfficerId = this.fromApiJsonHelper.extractLongNamed(fieldOfficerIdParamName, element); + if (fieldOfficerId != null) { + fieldOfficer = this.staffRepository.findOneWithNotFoundDetection(fieldOfficerId); + } + + final LocalDate submittedOnDate = this.fromApiJsonHelper.extractLocalDateNamed(submittedOnDateParamName, element); + + BigDecimal interestRate = null; + if (command.parameterExists(nominalAnnualInterestRateParamName)) { + interestRate = command.bigDecimalValueOfParameterNamed(nominalAnnualInterestRateParamName); + } else { + interestRate = product.nominalAnnualInterestRate(); + } + + SavingsCompoundingInterestPeriodType interestCompoundingPeriodType = null; + final Integer interestPeriodTypeValue = command.integerValueOfParameterNamed(interestCompoundingPeriodTypeParamName); + if (interestPeriodTypeValue != null) { + interestCompoundingPeriodType = SavingsCompoundingInterestPeriodType.fromInt(interestPeriodTypeValue); + } else { + interestCompoundingPeriodType = product.interestCompoundingPeriodType(); + } + + SavingsPostingInterestPeriodType interestPostingPeriodType = null; + final Integer interestPostingPeriodTypeValue = command.integerValueOfParameterNamed(interestPostingPeriodTypeParamName); + if (interestPostingPeriodTypeValue != null) { + interestPostingPeriodType = SavingsPostingInterestPeriodType.fromInt(interestPostingPeriodTypeValue); + } else { + interestPostingPeriodType = product.interestPostingPeriodType(); + } + + SavingsInterestCalculationType interestCalculationType = null; + final Integer interestCalculationTypeValue = command.integerValueOfParameterNamed(interestCalculationTypeParamName); + if (interestCalculationTypeValue != null) { + interestCalculationType = SavingsInterestCalculationType.fromInt(interestCalculationTypeValue); + } else { + interestCalculationType = product.interestCalculationType(); + } + + SavingsInterestCalculationDaysInYearType interestCalculationDaysInYearType = null; + final Integer interestCalculationDaysInYearTypeValue = command + .integerValueOfParameterNamed(interestCalculationDaysInYearTypeParamName); + if (interestCalculationDaysInYearTypeValue != null) { + interestCalculationDaysInYearType = SavingsInterestCalculationDaysInYearType.fromInt(interestCalculationDaysInYearTypeValue); + } else { + interestCalculationDaysInYearType = product.interestCalculationDaysInYearType(); + } + + BigDecimal minRequiredOpeningBalance = null; + if (command.parameterExists(minRequiredOpeningBalanceParamName)) { + minRequiredOpeningBalance = command.bigDecimalValueOfParameterNamed(minRequiredOpeningBalanceParamName); + } else { + minRequiredOpeningBalance = product.minRequiredOpeningBalance(); + } + + Integer lockinPeriodFrequency = null; + if (command.parameterExists(lockinPeriodFrequencyParamName)) { + lockinPeriodFrequency = command.integerValueOfParameterNamed(lockinPeriodFrequencyParamName); + } else { + lockinPeriodFrequency = product.lockinPeriodFrequency(); + } + + SavingsPeriodFrequencyType lockinPeriodFrequencyType = null; + Integer lockinPeriodFrequencyTypeValue = null; + if (command.parameterExists(lockinPeriodFrequencyTypeParamName)) { + lockinPeriodFrequencyTypeValue = command.integerValueOfParameterNamed(lockinPeriodFrequencyTypeParamName); + if (lockinPeriodFrequencyTypeValue != null) { + lockinPeriodFrequencyType = SavingsPeriodFrequencyType.fromInt(lockinPeriodFrequencyTypeValue); + } + } else { + lockinPeriodFrequencyType = product.lockinPeriodFrequencyType(); + } + boolean iswithdrawalFeeApplicableForTransfer = false; + if (command.parameterExists(withdrawalFeeForTransfersParamName)) { + iswithdrawalFeeApplicableForTransfer = command.booleanPrimitiveValueOfParameterNamed(withdrawalFeeForTransfersParamName); + } + + final Set charges = this.savingsAccountChargeAssembler.fromParsedJson(element, product.currency().getCode()); + + boolean allowOverdraft = false; + if (command.parameterExists(allowOverdraftParamName)) { + allowOverdraft = command.booleanPrimitiveValueOfParameterNamed(allowOverdraftParamName); + } else { + allowOverdraft = product.isAllowOverdraft(); + } + + BigDecimal overdraftLimit = BigDecimal.ZERO; + if (command.parameterExists(overdraftLimitParamName)) { + overdraftLimit = command.bigDecimalValueOfParameterNamedDefaultToNullIfZero(overdraftLimitParamName); + } else { + overdraftLimit = product.overdraftLimit(); + } + + BigDecimal nominalAnnualInterestRateOverdraft = BigDecimal.ZERO; + if (command.parameterExists(nominalAnnualInterestRateOverdraftParamName)) { + nominalAnnualInterestRateOverdraft = command + .bigDecimalValueOfParameterNamedDefaultToNullIfZero(nominalAnnualInterestRateOverdraftParamName); + } else { + nominalAnnualInterestRateOverdraft = product.nominalAnnualInterestRateOverdraft(); + } + + BigDecimal minOverdraftForInterestCalculation = BigDecimal.ZERO; + if (command.parameterExists(minOverdraftForInterestCalculationParamName)) { + minOverdraftForInterestCalculation = command + .bigDecimalValueOfParameterNamedDefaultToNullIfZero(minOverdraftForInterestCalculationParamName); + } else { + minOverdraftForInterestCalculation = product.minOverdraftForInterestCalculation(); + } + + boolean enforceMinRequiredBalance = false; + if (command.parameterExists(enforceMinRequiredBalanceParamName)) { + enforceMinRequiredBalance = command.booleanPrimitiveValueOfParameterNamed(enforceMinRequiredBalanceParamName); + } else { + enforceMinRequiredBalance = product.isMinRequiredBalanceEnforced(); + } + + BigDecimal minRequiredBalance = BigDecimal.ZERO; + if (command.parameterExists(minRequiredBalanceParamName)) { + minRequiredBalance = command.bigDecimalValueOfParameterNamedDefaultToNullIfZero(minRequiredBalanceParamName); + } else { + minRequiredBalance = product.minRequiredBalance(); + } + + boolean lienAllowed = false; + if (command.parameterExists(lienAllowedParamName)) { + lienAllowed = command.booleanPrimitiveValueOfParameterNamed(lienAllowedParamName); + } else { + lienAllowed = product.isLienAllowed(); + } + + BigDecimal maxAllowedLienLimit = BigDecimal.ZERO; + if (command.parameterExists(maxAllowedLienLimitParamName)) { + maxAllowedLienLimit = command.bigDecimalValueOfParameterNamedDefaultToNullIfZero(maxAllowedLienLimitParamName); + } else { + maxAllowedLienLimit = product.maxAllowedLienLimit(); + } + + boolean withHoldTax = product.withHoldTax(); + if (command.parameterExists(withHoldTaxParamName)) { + withHoldTax = command.booleanPrimitiveValueOfParameterNamed(withHoldTaxParamName); + if (withHoldTax && product.getTaxGroup() == null) { + throw new UnsupportedParameterException(Arrays.asList(withHoldTaxParamName)); + } + } + + final SavingsAccount account = SavingsAccount.createNewApplicationForSubmittal(client, group, product, fieldOfficer, accountNo, + externalIdFactory.create(externalId), accountType, submittedOnDate, submittedBy, interestRate, + interestCompoundingPeriodType, interestPostingPeriodType, interestCalculationType, interestCalculationDaysInYearType, + minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType, iswithdrawalFeeApplicableForTransfer, charges, + allowOverdraft, overdraftLimit, enforceMinRequiredBalance, minRequiredBalance, maxAllowedLienLimit, lienAllowed, + nominalAnnualInterestRateOverdraft, minOverdraftForInterestCalculation, withHoldTax); + account.setHelpers(this.savingsAccountTransactionSummaryWrapper, this.savingsHelper); + + account.validateNewApplicationState(SAVINGS_ACCOUNT_RESOURCE_NAME); + + account.validateAccountValuesWithProduct(); + + // CBS extended fields + applyCbsFields(command, element, account); + + return account; + } + + private static final Gson GSON = new Gson(); + + public void applyCbsFields(final JsonCommand command, final JsonElement element, final SavingsAccount account) { + // Product classification + final String cbsAccountType = this.fromApiJsonHelper.extractStringNamed(accountTypeParamName, element); + if (cbsAccountType != null) account.setCbsAccountType(cbsAccountType); + + final String customerSegment = this.fromApiJsonHelper.extractStringNamed(customerSegmentParamName, element); + if (customerSegment != null) account.setCustomerSegment(customerSegment); + + // Status of Account + final String accountStatus = this.fromApiJsonHelper.extractStringNamed(accountStatusParamName, element); + if (accountStatus != null) account.setAccountStatus(accountStatus); + + final String modeOfOperation = this.fromApiJsonHelper.extractStringNamed(modeOfOperationParamName, element); + if (modeOfOperation != null) account.setModeOfOperation(modeOfOperation); + + account.setSpecialCategory(this.fromApiJsonHelper.extractStringNamed(specialCategoryParamName, element)); + account.setWeakerSection(this.fromApiJsonHelper.extractStringNamed(weakerSectionParamName, element)); + + if (command.parameterExists(nominationFacilityParamName)) { + account.setNominationFacility(this.fromApiJsonHelper.extractBooleanNamed(nominationFacilityParamName, element)); + } + if (command.parameterExists(autoDebitFacilityParamName)) { + account.setAutoDebitFacility(this.fromApiJsonHelper.extractBooleanNamed(autoDebitFacilityParamName, element)); + } + + // Senior citizen rate — runs after validateDomainRules so it is the final persisted value + if (command.parameterExists(isSeniorCitizenParamName)) { + final Boolean scFlag = this.fromApiJsonHelper.extractBooleanNamed(isSeniorCitizenParamName, element); + if (scFlag != null) { + account.setSeniorCitizen(scFlag); + if (Boolean.TRUE.equals(scFlag) && command.parameterExists(seniorCitizenInterestRateParamName)) { + final BigDecimal scRate = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed( + seniorCitizenInterestRateParamName, element); + if (scRate != null && scRate.compareTo(BigDecimal.ZERO) > 0) { + account.setSeniorCitizenInterestRate(scRate); + account.updateNominalAnnualInterestRate(scRate); + } + } + } + } + + account.setGuardianName(this.fromApiJsonHelper.extractStringNamed(guardianNameParamName, element)); + account.setGuardianCifNumber(this.fromApiJsonHelper.extractStringNamed(guardianCifNumberParamName, element)); + account.setGuardianRelationship(this.fromApiJsonHelper.extractStringNamed(guardianRelationshipParamName, element)); + + if (command.parameterExists(minorDateOfBirthParamName)) { + final LocalDate minorDob = this.fromApiJsonHelper.extractLocalDateNamed(minorDateOfBirthParamName, element); + account.setMinorDateOfBirth(minorDob); + } + + final Long guardianClientId = this.fromApiJsonHelper.extractLongNamed(guardianClientIdParamName, element); + if (guardianClientId != null) account.setGuardianClientId(guardianClientId); + + // Introducer + final Long introducerId = this.fromApiJsonHelper.extractLongNamed(introducerIdParamName, element); + if (introducerId != null) account.setIntroducerClientId(introducerId); + account.setIntroducerName(this.fromApiJsonHelper.extractStringNamed(introducerNameParamName, element)); + account.setIntroducerCifNumber(this.fromApiJsonHelper.extractStringNamed(introducerCifNumberParamName, element)); + + // Nominee (stored as JSON string) + if (element.getAsJsonObject().has(nomineeParamName)) { + final JsonElement nomineeEl = element.getAsJsonObject().get(nomineeParamName); + if (nomineeEl != null && !nomineeEl.isJsonNull()) { + account.setNomineeData(nomineeEl.toString()); + } + } + + // Deposit Account Link + final Long linkedAccountId = this.fromApiJsonHelper.extractLongNamed(linkedAccountIdParamName, element); + if (linkedAccountId != null) account.setLinkedAccountId(linkedAccountId); + account.setLinkedAccountType(this.fromApiJsonHelper.extractStringNamed(linkedAccountTypeParamName, element)); + account.setLinkedAccountNo(this.fromApiJsonHelper.extractStringNamed(linkedAccountNoParamName, element)); + + // Account Facility Config + if (element.getAsJsonObject().has(accountFacilityConfigParamName)) { + final JsonElement configEl = element.getAsJsonObject().get(accountFacilityConfigParamName); + if (configEl != null && !configEl.isJsonNull() && configEl.isJsonObject()) { + SavingsAccountFacilityConfig facilityConfig = account.getFacilityConfig(); + if (facilityConfig == null) { + facilityConfig = new SavingsAccountFacilityConfig(account); + account.setFacilityConfig(facilityConfig); + } + facilityConfig.updateFrom(configEl.getAsJsonObject()); + } + } + + // Joint holders + if (element.getAsJsonObject().has(jointHoldersParamName)) { + final JsonElement holdersEl = element.getAsJsonObject().get(jointHoldersParamName); + if (holdersEl != null && !holdersEl.isJsonNull() && holdersEl.isJsonArray()) { + final com.google.gson.JsonArray holdersArray = holdersEl.getAsJsonArray(); + final List holders = new ArrayList<>(); + for (int i = 0; i < holdersArray.size(); i++) { + final JsonObject h = holdersArray.get(i).getAsJsonObject(); + final Long clientId = h.has("clientId") && !h.get("clientId").isJsonNull() + ? h.get("clientId").getAsLong() : null; + final String cifNumber = h.has("cifNumber") && !h.get("cifNumber").isJsonNull() + ? h.get("cifNumber").getAsString() : null; + final String displayName = h.has("displayName") && !h.get("displayName").isJsonNull() + ? h.get("displayName").getAsString() : null; + final BigDecimal sharePercentage = h.has("sharePercentage") && !h.get("sharePercentage").isJsonNull() + ? h.get("sharePercentage").getAsBigDecimal() : null; + final boolean isPrimary = h.has("isPrimary") && h.get("isPrimary").getAsBoolean(); + String signingAuthority = h.has("signingAuthority") && !h.get("signingAuthority").isJsonNull() + ? h.get("signingAuthority").getAsString() : null; + String role = h.has("role") && !h.get("role").isJsonNull() + ? h.get("role").getAsString() : null; + + if (signingAuthority == null || signingAuthority.trim().isEmpty()) { + signingAuthority = isPrimary ? "Mandatory" : "Optional"; + } + if (role == null || role.trim().isEmpty()) { + role = isPrimary ? "Primary Account Holder" : "Co-Holder"; + } + + final boolean isMandatory = h.has("isMandatory") && h.get("isMandatory").getAsBoolean(); + holders.add(new SavingsAccountJointHolder(account, clientId, cifNumber, displayName, + sharePercentage, signingAuthority, role, isPrimary, isMandatory)); + } + account.getJointHolders().clear(); + account.getJointHolders().addAll(holders); + } + } + } + + public SavingsAccount assembleFrom(final Long savingsId, final boolean backdatedTxnsAllowedTill) { + SavingsAccount account = this.savingsAccountRepository.findSavingsWithNotFoundDetection(savingsId, backdatedTxnsAllowedTill); + return loadTransactionsToSavingsAccount(account, backdatedTxnsAllowedTill); + } + + public SavingsAccount loadTransactionsToSavingsAccount(final SavingsAccount account, final boolean backdatedTxnsAllowedTill) { + List savingsAccountTransactions = null; + if (backdatedTxnsAllowedTill) { + LocalDate pivotDate = account.getSummary().getInterestPostedTillDate(); + boolean isNotPresent = pivotDate == null; + if (!isNotPresent) { + // Get savings account transactions + if (isRelaxingDaysConfigForPivotDateEnabled()) { + final Long relaxingDaysForPivotDate = this.configurationDomainService.retrieveRelaxingDaysConfigForPivotDate(); + LocalDate interestPostedTillDate = account.getSummary().getInterestPostedTillDate(); + savingsAccountTransactions = this.savingsAccountRepository.findTransactionsAfterPivotDate(account, + interestPostedTillDate.minusDays(relaxingDaysForPivotDate)); + + savingsAccountTransactions.get(0).getSavingsAccount() + .setStartInterestCalculationDate(interestPostedTillDate.minusDays(relaxingDaysForPivotDate)); + List pivotDateTransaction = this.savingsAccountRepository + .findTransactionRunningBalanceBeforePivotDate(account, + interestPostedTillDate.minusDays(relaxingDaysForPivotDate + 1)); + if (pivotDateTransaction != null && !pivotDateTransaction.isEmpty()) { + account.getSummary().setRunningBalanceOnPivotDate(pivotDateTransaction.get(pivotDateTransaction.size() - 1) + .getRunningBalance(account.getCurrency()).getAmount()); + } + } else { + savingsAccountTransactions = this.savingsAccountRepository.findTransactionsAfterPivotDate(account, + account.getSummary().getInterestPostedTillDate()); + } + + if (savingsAccountTransactions != null && savingsAccountTransactions.size() > 0) { + // Update transient variable + account.setSavingsAccountTransactions(savingsAccountTransactions); + } + } else { + savingsAccountTransactions = this.savingsAccountRepository.findAllTransactions(account); + account.setSavingsAccountTransactions(savingsAccountTransactions); + } + } + + account.setHelpers(this.savingsAccountTransactionSummaryWrapper, this.savingsHelper); + return account; + } + + public SavingsAccountData assembleSavings(final SavingsAccountData account) { + + // Update last running balance on account level + final boolean backdatedTxnsAllowedTill = this.configurationDomainService.retrievePivotDateConfig(); + if (backdatedTxnsAllowedTill && account.getSavingsAccountTransactionData() != null + && account.getSummary().getInterestPostedTillDate() != null) { + List removalList = new ArrayList<>(); + + for (int i = 0; i < account.getSavingsAccountTransactionData().size(); i++) { + SavingsAccountTransactionData savingsAccountTransaction = account.getSavingsAccountTransactionData().get(i); + removalList.add(savingsAccountTransaction); + if ((savingsAccountTransaction.isInterestPostingAndNotReversed() + || savingsAccountTransaction.isOverdraftInterestAndNotReversed()) + && !savingsAccountTransaction.isReversalTransaction()) { + account.getSummary().setRunningBalanceOnPivotDate(savingsAccountTransaction.getRunningBalance()); + account.setLastSavingsAccountTransaction(savingsAccountTransaction); + break; + } + } + account.getSavingsAccountTransactionData().removeAll(removalList); + } else { + account.getSummary().setRunningBalanceOnPivotDate(BigDecimal.ZERO); + } + account.setHelpers(this.savingsAccountTransactionDataSummaryWrapper, this.savingsHelper); + return account; + } + + public boolean getPivotConfigStatus() { + return this.configurationDomainService.retrievePivotDateConfig(); + } + + public boolean isRelaxingDaysConfigForPivotDateEnabled() { + return this.configurationDomainService.isRelaxingDaysConfigForPivotDateEnabled(); + } + + public void setHelpers(final SavingsAccount account) { + account.setHelpers(this.savingsAccountTransactionSummaryWrapper, this.savingsHelper); + } + + /** + * Assembles a new {@link SavingsAccount} from JSON details passed in request inheriting details where relevant from + * chosen {@link SavingsProduct}. + */ + public SavingsAccount assembleFrom(final Client client, final Group group, final Long productId, final LocalDate appliedonDate, + final AppUser appliedBy) { + + AccountType accountType = AccountType.INVALID; + if (client != null) { + accountType = AccountType.INDIVIDUAL; + if (client.isNotActive()) { + throw new ClientNotActiveException(client.getId()); + } + } + + if (group != null) { + accountType = AccountType.GROUP; + if (group.isNotActive()) { + if (group.isCenter()) { + throw new CenterNotActiveException(group.getId()); + } + throw new GroupNotActiveException(group.getId()); + } + } + + if (group != null && client != null) { + if (!group.hasClientAsMember(client)) { + throw new ClientNotInGroupException(client.getId(), group.getId()); + } + accountType = AccountType.JLG; + } + final SavingsProduct product = this.savingProductRepository.findById(productId).orElseThrow(); + final Set charges = this.savingsAccountChargeAssembler.fromSavingsProduct(product); + final SavingsAccount account = SavingsAccount.createNewApplicationForSubmittal(client, group, product, null, null, null, + accountType, appliedonDate, appliedBy, product.nominalAnnualInterestRate(), product.interestCompoundingPeriodType(), + product.interestPostingPeriodType(), product.interestCalculationType(), product.interestCalculationDaysInYearType(), + product.minRequiredOpeningBalance(), product.lockinPeriodFrequency(), product.lockinPeriodFrequencyType(), + product.isWithdrawalFeeApplicableForTransfer(), charges, product.isAllowOverdraft(), product.overdraftLimit(), + product.isMinRequiredBalanceEnforced(), product.minRequiredBalance(), product.maxAllowedLienLimit(), + product.isLienAllowed(), product.nominalAnnualInterestRateOverdraft(), product.minOverdraftForInterestCalculation(), + product.withHoldTax()); + account.setHelpers(this.savingsAccountTransactionSummaryWrapper, this.savingsHelper); + + account.validateNewApplicationState(SAVINGS_ACCOUNT_RESOURCE_NAME); + + account.validateAccountValuesWithProduct(); + + return account; + } + + public void assignSavingAccountHelpers(final SavingsAccount savingsAccount) { + savingsAccount.setHelpers(this.savingsAccountTransactionSummaryWrapper, this.savingsHelper); + } + + public void assignSavingAccountHelpers(final SavingsAccountData savingsAccountData) { + savingsAccountData.setHelpers(this.savingsAccountTransactionDataSummaryWrapper, this.savingsHelper); + } +} diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index 09f6a936b..592ae3103 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -316,4 +316,5 @@ + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0300_rename_fd_base_interest_rate_tables_to_cbs.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0300_rename_fd_base_interest_rate_tables_to_cbs.xml new file mode 100644 index 000000000..c75abc79e --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0300_rename_fd_base_interest_rate_tables_to_cbs.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java index 60dfa4ec3..5d2cb13ca 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java @@ -3969,6 +3969,12 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom @Column(name = "auto_debit_facility", nullable = false, columnDefinition = "boolean default false") private Boolean autoDebitFacility = false; + @Column(name = "is_senior_citizen", nullable = false, columnDefinition = "boolean default false") + private boolean isSeniorCitizen = false; + + @Column(name = "senior_citizen_interest_rate", scale = 6, precision = 19, nullable = true) + private BigDecimal seniorCitizenInterestRate; + @jakarta.persistence.OneToOne(cascade = jakarta.persistence.CascadeType.ALL, mappedBy = "savingsAccount", orphanRemoval = true, fetch = FetchType.LAZY) private SavingsAccountFacilityConfig facilityConfig; @@ -4041,4 +4047,16 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom public Boolean getAutoDebitFacility() { return autoDebitFacility; } public void setAutoDebitFacility(Boolean v) { this.autoDebitFacility = v; } + + public boolean isSeniorCitizen() { return isSeniorCitizen; } + public void setSeniorCitizen(boolean v) { this.isSeniorCitizen = v; } + + public BigDecimal getSeniorCitizenInterestRate() { return seniorCitizenInterestRate; } + public void setSeniorCitizenInterestRate(BigDecimal v) { this.seniorCitizenInterestRate = v; } + + public void updateNominalAnnualInterestRate(BigDecimal rate) { + if (rate != null && rate.compareTo(BigDecimal.ZERO) > 0) { + this.nominalAnnualInterestRate = rate; + } + } } diff --git a/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/module-changelog-master.xml b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/module-changelog-master.xml index e3a40fc1b..f8c193c0b 100644 --- a/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/module-changelog-master.xml +++ b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/module-changelog-master.xml @@ -1,40 +1,41 @@ - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2015_add_senior_citizen_to_savings_account.xml b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2015_add_senior_citizen_to_savings_account.xml new file mode 100644 index 000000000..6031e6afa --- /dev/null +++ b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2015_add_senior_citizen_to_savings_account.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + From 45585ecc10648608605a96f7e5e0da847cff829f Mon Sep 17 00:00:00 2001 From: Aditya Date: Fri, 22 May 2026 13:06:38 +0530 Subject: [PATCH 02/10] Fix family-member PUT 500; CORS POST 403; Hikari local password default - ClientFamilyMembersWritePlatformServiceImpl#updateFamilyMember: guard maritalStatusId/genderId/professionId with != null instead of != 0 so a payload that omits these keys doesn't NPE when the boxed Long is unboxed. - application.properties: fix the broken property name `fineract.security.cors.allowed-metho pas` -> `allowed-methods`. The typo left CORS allowed-methods empty, which made Spring reject every browser POST with "Invalid CORS request" 403 once the SPA sent an Origin header. - application.properties: change the Hikari default password from 313863 to root so the local devRun matches the tenant-DB default at line 49. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../ClientFamilyMembersWritePlatformServiceImpl.java | 6 +++--- fineract-provider/src/main/resources/application.properties | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientFamilyMembersWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientFamilyMembersWritePlatformServiceImpl.java index 029cd82e2..1e9d342d9 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientFamilyMembersWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientFamilyMembersWritePlatformServiceImpl.java @@ -463,19 +463,19 @@ public class ClientFamilyMembersWritePlatformServiceImpl implements ClientFamily clientFamilyMember.setRelationship(relationship); } - if (command.longValueOfParameterNamed("maritalStatusId") != 0) { + if (command.longValueOfParameterNamed("maritalStatusId") != null) { maritalStatusId = command.longValueOfParameterNamed("maritalStatusId"); maritalStatus = this.codeValueRepository.getReferenceById(maritalStatusId); clientFamilyMember.setMaritalStatus(maritalStatus); } - if (command.longValueOfParameterNamed("genderId") != 0) { + if (command.longValueOfParameterNamed("genderId") != null) { genderId = command.longValueOfParameterNamed("genderId"); gender = this.codeValueRepository.getReferenceById(genderId); clientFamilyMember.setGender(gender); } - if (command.longValueOfParameterNamed("professionId") != 0) { + if (command.longValueOfParameterNamed("professionId") != null) { professionId = command.longValueOfParameterNamed("professionId"); profession = this.codeValueRepository.getReferenceById(professionId); clientFamilyMember.setProfession(profession); diff --git a/fineract-provider/src/main/resources/application.properties b/fineract-provider/src/main/resources/application.properties index 3432b0ff3..696393b9e 100644 --- a/fineract-provider/src/main/resources/application.properties +++ b/fineract-provider/src/main/resources/application.properties @@ -407,7 +407,7 @@ server.tomcat.mbeanregistry.enabled=${FINERACT_SERVER_TOMCAT_MBEANREGISTRY_ENABL spring.datasource.hikari.driverClassName=${FINERACT_HIKARI_DRIVER_SOURCE_CLASS_NAME:org.postgresql.Driver} spring.datasource.hikari.jdbcUrl=${FINERACT_HIKARI_JDBC_URL:jdbc:postgresql://localhost:5432/fineract_tenants} spring.datasource.hikari.username=${FINERACT_HIKARI_USERNAME:postgres} -spring.datasource.hikari.password=${FINERACT_HIKARI_PASSWORD:313863} +spring.datasource.hikari.password=${FINERACT_HIKARI_PASSWORD:root} spring.datasource.hikari.minimumIdle=${FINERACT_HIKARI_MINIMUM_IDLE:3} spring.datasource.hikari.maximumPoolSize=${FINERACT_HIKARI_MAXIMUM_POOL_SIZE:10} spring.datasource.hikari.idleTimeout=${FINERACT_HIKARI_IDLE_TIMEOUT:60000} From 2251fd225976981bde3fd52599e6910e206244f5 Mon Sep 17 00:00:00 2001 From: Jaanu423 Date: Tue, 26 May 2026 10:55:35 +0530 Subject: [PATCH 03/10] feat: current account standing instructions - full backend support - StandingInstructionData: rename conflicting Lombok getters (getInstructionType, getRecurrenceType, getRecurrenceFrequency, getFromAccountType, getToAccountType, getTransferType) to domain enum methods so Jackson serializes EnumOptionData fields as {id, code, value} objects instead of plain strings - AccountTransfersWritePlatformServiceImpl: add current account transfer support for all 4 missing cases (Current->Savings, Savings->Current, Current->Current, Current->Loan) with performCurrentWithdrawal/performCurrentDeposit helpers - AccountConfiguration: wire CurrentAccountRepository and CurrentAccountTransactionRepository into AccountTransfersWritePlatformService bean - StandingInstructionApiResource: fix getValue() type mismatch after getter rename - ExecuteStandingInstructionsTasklet: update to use renamed enum accessor methods - PortfolioAccountType: add CURRENT(3) type support - DB migration: 0301_add_current_account_to_transfer_details.xml adds from_current_account_id and to_current_account_id columns to transfer details table Co-Authored-By: Claude Sonnet 4.6 --- .../account/PortfolioAccountType.java | 150 +- .../api/StandingInstructionApiResource.java | 440 +++--- .../AccountTransfersDetailDataValidator.java | 158 +- .../account/data/StandingInstructionData.java | 688 ++++----- .../StandingInstructionDataValidator.java | 590 ++++---- .../AccountTransferDetailAssembler.java | 528 ++++--- .../domain/AccountTransferDetails.java | 381 ++--- .../AccountTransferStandingInstruction.java | 592 ++++---- .../domain/StandingInstructionAssembler.java | 236 +-- .../ExecuteStandingInstructionsTasklet.java | 344 ++--- .../service/AccountTransferEnumerations.java | 408 ++--- ...ountTransfersWritePlatformServiceImpl.java | 1314 ++++++++++------- ...rtfolioAccountReadPlatformServiceImpl.java | 878 ++++++----- ...ingInstructionReadPlatformServiceImpl.java | 1216 +++++++-------- ...ngInstructionWritePlatformServiceImpl.java | 324 ++-- .../account/starter/AccountConfiguration.java | 272 ++-- .../src/main/resources/application.properties | 2 +- .../db/changelog/tenant/changelog-tenant.xml | 1 + ...dd_current_account_to_transfer_details.xml | 56 + 19 files changed, 4621 insertions(+), 3957 deletions(-) create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0301_add_current_account_to_transfer_details.xml diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/account/PortfolioAccountType.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/account/PortfolioAccountType.java index 3a81a147c..7b32373fe 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/account/PortfolioAccountType.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/account/PortfolioAccountType.java @@ -1,71 +1,79 @@ -/** - * 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.account; - -public enum PortfolioAccountType { - - INVALID(0, "accountType.invalid"), // - LOAN(1, "accountType.loan"), // - SAVINGS(2, "accountType.savings"), // - ; - - private final Integer value; - private final String code; - - PortfolioAccountType(final Integer value, final String code) { - this.value = value; - this.code = code; - } - - public Integer getValue() { - return this.value; - } - - public String getCode() { - return this.code; - } - - public static PortfolioAccountType fromInt(final Integer type) { - - PortfolioAccountType enumType = INVALID; - if (type != null) { - switch (type) { - case 1: - enumType = LOAN; - break; - case 2: - enumType = SAVINGS; - break; - default: - enumType = INVALID; - } - } - return enumType; - } - - // TODO: bad practice and unnecessary code! why not just use the enum values themselves!?! - public boolean isSavingsAccount() { - return this.equals(SAVINGS); - } - - // TODO: bad practice and unnecessary code! why not just use the enum values themselves!?! - public boolean isLoanAccount() { - return this.equals(LOAN); - } -} +/** + * 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.account; + +public enum PortfolioAccountType { + + INVALID(0, "accountType.invalid"), // + LOAN(1, "accountType.loan"), // + SAVINGS(2, "accountType.savings"), // + CURRENT(3, "accountType.current"), // + ; + + private final Integer value; + private final String code; + + PortfolioAccountType(final Integer value, final String code) { + this.value = value; + this.code = code; + } + + public Integer getValue() { + return this.value; + } + + public String getCode() { + return this.code; + } + + public static PortfolioAccountType fromInt(final Integer type) { + + PortfolioAccountType enumType = INVALID; + if (type != null) { + switch (type) { + case 1: + enumType = LOAN; + break; + case 2: + enumType = SAVINGS; + break; + case 3: + enumType = CURRENT; + break; + default: + enumType = INVALID; + } + } + return enumType; + } + + // TODO: bad practice and unnecessary code! why not just use the enum values themselves!?! + public boolean isSavingsAccount() { + return this.equals(SAVINGS); + } + + // TODO: bad practice and unnecessary code! why not just use the enum values themselves!?! + public boolean isLoanAccount() { + return this.equals(LOAN); + } + + public boolean isCurrentAccount() { + return this.equals(CURRENT); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/api/StandingInstructionApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/api/StandingInstructionApiResource.java index b443b343d..68309f923 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/api/StandingInstructionApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/api/StandingInstructionApiResource.java @@ -1,220 +1,220 @@ -/** - * 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.account.api; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.parameters.RequestBody; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.ws.rs.BeanParam; -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.PUT; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.QueryParam; -import jakarta.ws.rs.core.Context; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.UriInfo; -import java.time.LocalDate; -import java.util.Arrays; -import java.util.Map; -import java.util.Set; -import lombok.RequiredArgsConstructor; -import org.apache.fineract.batch.command.CommandHandlerRegistry; -import org.apache.fineract.commands.domain.CommandWrapper; -import org.apache.fineract.commands.service.CommandWrapperBuilder; -import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService; -import org.apache.fineract.infrastructure.core.api.ApiParameterHelper; -import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; -import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException; -import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; -import org.apache.fineract.infrastructure.core.service.CommandParameterUtil; -import org.apache.fineract.infrastructure.core.service.Page; -import org.apache.fineract.infrastructure.core.service.SearchParameters; -import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; -import org.apache.fineract.infrastructure.security.service.SqlValidator; -import org.apache.fineract.portfolio.account.data.AccountTransferData; -import org.apache.fineract.portfolio.account.data.StandingInstructionDTO; -import org.apache.fineract.portfolio.account.data.StandingInstructionData; -import org.apache.fineract.portfolio.account.data.request.StandingInstructionCreationRequest; -import org.apache.fineract.portfolio.account.data.request.StandingInstructionSearchParam; -import org.apache.fineract.portfolio.account.data.request.StandingInstructionUpdatesRequest; -import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService; -import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformService; -import org.springframework.stereotype.Component; - -@Path("/v1/standinginstructions") -@Component -@Tag(name = "Standing Instructions", description = "Standing instructions (or standing orders) refer to instructions a bank account holder (\"the payer\") gives to his or her bank to pay a set amount at regular intervals to another's (\"the payee's\") account.\n" - + "\n" + "Note: At present only savings account to savings account and savings account to Loan account transfers are permitted.") -@RequiredArgsConstructor -public class StandingInstructionApiResource { - - private final PlatformSecurityContext context; - private final DefaultToApiJsonSerializer toApiJsonSerializer; - private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; - private final StandingInstructionReadPlatformService standingInstructionReadPlatformService; - private final AccountTransfersReadPlatformService accountTransfersReadPlatformService; - private final SqlValidator sqlValidator; - - private static final CommandHandlerRegistry COMMAND_HANDLER_REGISTRY = new CommandHandlerRegistry<>( - Map.of(CommandParameterUtil.UPDATE_COMMAND_VALUE, - (id, json) -> new CommandWrapperBuilder().updateStandingInstruction(id).withJson(json).build(), - CommandParameterUtil.DELETE_COMMAND_VALUE, - (id, json) -> new CommandWrapperBuilder().deleteStandingInstruction(id).withJson(json).build())); - - @GET - @Path("template") - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - @Operation(summary = "Retrieve Standing Instruction Template", description = "This is a convenience resource. " - + "It can be useful when building maintenance user interface screens for client applications. " - + "The template data returned consists of any or all of:\n" + "\n" + "Field Defaults\n" + "Allowed Value Lists\n" - + "Example Requests:\n" + "\n" + "standinginstructions/template?fromAccountType=2&fromOfficeId=1\n" + "\n" - + "standinginstructions/template?fromAccountType=2&fromOfficeId=1&fromClientId=1&transferType=1\n" + "\n" - + "standinginstructions/template?fromClientId=1&fromAccountType=2&fromAccountId=1&transferType=1") - @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StandingInstructionApiResourceSwagger.GetStandingInstructionsTemplateResponse.class))) - public StandingInstructionData template(@BeanParam StandingInstructionSearchParam instructionParam) { - context.authenticatedUser().validateHasReadPermission(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME); - - return standingInstructionReadPlatformService.retrieveTemplate(instructionParam.getFromOfficeId(), - instructionParam.getFromClientId(), instructionParam.getFromAccountId(), instructionParam.getFromAccountType(), - instructionParam.getToOfficeId(), instructionParam.getToClientId(), instructionParam.getToAccountId(), - instructionParam.getToAccountType(), instructionParam.getTransferType()); - } - - @POST - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - @Operation(summary = "Create new Standing Instruction", description = "Ability to create new instruction for transfer of monetary funds from one account to another") - @RequestBody(required = true, content = @Content(schema = @Schema(implementation = StandingInstructionCreationRequest.class))) - @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StandingInstructionApiResourceSwagger.PostStandingInstructionsResponse.class))) - public CommandProcessingResult create(@Parameter(hidden = true) StandingInstructionCreationRequest creationRequest) { - final CommandWrapper commandRequest = new CommandWrapperBuilder().createStandingInstruction() - .withJson(toApiJsonSerializer.serialize(creationRequest)).build(); - - return commandsSourceWritePlatformService.logCommandSource(commandRequest); - } - - @PUT - @Path("{standingInstructionId}") - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - @Operation(summary = "Update Standing Instruction | Delete Standing Instruction", description = "Ability to modify existing instruction for transfer of monetary funds from one account to another.\n" - + "\n" + "PUT https://DomainName/api/v1/standinginstructions/1?command=update\n" + "\n\n" - + "Ability to modify existing instruction for transfer of monetary funds from one account to another.\n" + "\n" - + "PUT https://DomainName/api/v1/standinginstructions/1?command=delete") - @RequestBody(content = @Content(schema = @Schema(implementation = StandingInstructionUpdatesRequest.class))) - @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StandingInstructionApiResourceSwagger.PutStandingInstructionsStandingInstructionIdResponse.class))) - public CommandProcessingResult update( - @PathParam("standingInstructionId") @Parameter(description = "standingInstructionId") final Long standingInstructionId, - @Parameter(hidden = true) StandingInstructionUpdatesRequest updatesRequest, - @QueryParam("command") @Parameter(description = "command") final String commandParam) { - - final String serializedUpdatesRequest = toApiJsonSerializer.serialize(updatesRequest); - final CommandWrapper commandRequest = COMMAND_HANDLER_REGISTRY.execute(commandParam, standingInstructionId, - serializedUpdatesRequest, new UnrecognizedQueryParamException("command", commandParam)); - - return commandsSourceWritePlatformService.logCommandSource(commandRequest); - } - - @GET - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - @Operation(summary = "List Standing Instructions", description = "Example Requests:\n" + "\n" + "standinginstructions") - @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StandingInstructionApiResourceSwagger.GetStandingInstructionsResponse.class))) - public Page retrieveAll( - @QueryParam("externalId") @Parameter(description = "externalId") final String externalId, - @QueryParam("offset") @Parameter(description = "offset") final Integer offset, - @QueryParam("limit") @Parameter(description = "limit") final Integer limit, - @QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy, - @QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder, - @QueryParam("transferType") @Parameter(description = "transferType") final Integer transferType, - @QueryParam("clientName") @Parameter(description = "clientName") final String clientName, - @QueryParam("clientId") @Parameter(description = "clientId") final Long clientId, - @QueryParam("fromAccountId") @Parameter(description = "fromAccountId") final Long fromAccount, - @QueryParam("fromAccountType") @Parameter(description = "fromAccountType") final Integer fromAccountType) { - - context.authenticatedUser().validateHasReadPermission(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME); - - sqlValidator.validate(orderBy); - sqlValidator.validate(sortOrder); - sqlValidator.validate(externalId); - final SearchParameters searchParameters = SearchParameters.builder().limit(limit).externalId(externalId).offset(offset) - .orderBy(orderBy).sortOrder(sortOrder).build(); - - final LocalDate startDateRange = null; - final LocalDate endDateRange = null; - StandingInstructionDTO standingInstructionDTO = new StandingInstructionDTO(searchParameters, transferType, clientName, clientId, - fromAccount, fromAccountType, startDateRange, endDateRange); - - return standingInstructionReadPlatformService.retrieveAll(standingInstructionDTO); - } - - @GET - @Path("{standingInstructionId}") - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - @Operation(summary = "Retrieve Standing Instruction", description = "Example Requests :\n" + "\n" + "standinginstructions/1") - @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StandingInstructionApiResourceSwagger.GetStandingInstructionsStandingInstructionIdResponse.class))) - public StandingInstructionData retrieveOne( - @PathParam("standingInstructionId") @Parameter(description = "standingInstructionId") final Long standingInstructionId, - @Context final UriInfo uriInfo, @QueryParam("externalId") @Parameter(description = "externalId") final String externalId, - @QueryParam("offset") @Parameter(description = "offset") final Integer offset, - @QueryParam("limit") @Parameter(description = "limit") final Integer limit, - @QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy, - @QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder) { - - context.authenticatedUser().validateHasReadPermission(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME); - - sqlValidator.validate(orderBy); - sqlValidator.validate(sortOrder); - StandingInstructionData standingInstructionData = standingInstructionReadPlatformService.retrieveOne(standingInstructionId); - final SearchParameters searchParameters = SearchParameters.builder().limit(limit).externalId(externalId).offset(offset) - .orderBy(orderBy).sortOrder(sortOrder).build(); - final Set associationParameters = ApiParameterHelper.extractAssociationsForResponseIfProvided(uriInfo.getQueryParameters()); - if (!associationParameters.isEmpty()) { - if (associationParameters.contains("all")) { - associationParameters.addAll(Arrays.asList("transactions", "template")); - } - if (associationParameters.contains("transactions")) { - Page transfers = accountTransfersReadPlatformService - .retrieveByStandingInstruction(standingInstructionId, searchParameters); - standingInstructionData = StandingInstructionData.withTransferData(standingInstructionData, transfers); - } - if (associationParameters.contains("template")) { - final StandingInstructionData templateData = standingInstructionReadPlatformService.retrieveTemplate( - standingInstructionData.getFromClient().getOfficeId(), standingInstructionData.getFromClient().getId(), - standingInstructionData.getFromAccount().getId(), standingInstructionData.getFromAccountType().getValue(), - standingInstructionData.getToClient().getOfficeId(), standingInstructionData.getToClient().getId(), - standingInstructionData.getToAccount().getId(), standingInstructionData.getToAccountType().getValue(), - standingInstructionData.getTransferType().getValue()); - standingInstructionData = StandingInstructionData.withTemplateData(standingInstructionData, templateData); - } - } - - return standingInstructionData; - } -} +/** + * 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.account.api; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.BeanParam; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.UriInfo; +import java.time.LocalDate; +import java.util.Arrays; +import java.util.Map; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.batch.command.CommandHandlerRegistry; +import org.apache.fineract.commands.domain.CommandWrapper; +import org.apache.fineract.commands.service.CommandWrapperBuilder; +import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService; +import org.apache.fineract.infrastructure.core.api.ApiParameterHelper; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException; +import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; +import org.apache.fineract.infrastructure.core.service.CommandParameterUtil; +import org.apache.fineract.infrastructure.core.service.Page; +import org.apache.fineract.infrastructure.core.service.SearchParameters; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.infrastructure.security.service.SqlValidator; +import org.apache.fineract.portfolio.account.data.AccountTransferData; +import org.apache.fineract.portfolio.account.data.StandingInstructionDTO; +import org.apache.fineract.portfolio.account.data.StandingInstructionData; +import org.apache.fineract.portfolio.account.data.request.StandingInstructionCreationRequest; +import org.apache.fineract.portfolio.account.data.request.StandingInstructionSearchParam; +import org.apache.fineract.portfolio.account.data.request.StandingInstructionUpdatesRequest; +import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService; +import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformService; +import org.springframework.stereotype.Component; + +@Path("/v1/standinginstructions") +@Component +@Tag(name = "Standing Instructions", description = "Standing instructions (or standing orders) refer to instructions a bank account holder (\"the payer\") gives to his or her bank to pay a set amount at regular intervals to another's (\"the payee's\") account.\n" + + "\n" + "Note: At present only savings account to savings account and savings account to Loan account transfers are permitted.") +@RequiredArgsConstructor +public class StandingInstructionApiResource { + + private final PlatformSecurityContext context; + private final DefaultToApiJsonSerializer toApiJsonSerializer; + private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; + private final StandingInstructionReadPlatformService standingInstructionReadPlatformService; + private final AccountTransfersReadPlatformService accountTransfersReadPlatformService; + private final SqlValidator sqlValidator; + + private static final CommandHandlerRegistry COMMAND_HANDLER_REGISTRY = new CommandHandlerRegistry<>( + Map.of(CommandParameterUtil.UPDATE_COMMAND_VALUE, + (id, json) -> new CommandWrapperBuilder().updateStandingInstruction(id).withJson(json).build(), + CommandParameterUtil.DELETE_COMMAND_VALUE, + (id, json) -> new CommandWrapperBuilder().deleteStandingInstruction(id).withJson(json).build())); + + @GET + @Path("template") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Retrieve Standing Instruction Template", description = "This is a convenience resource. " + + "It can be useful when building maintenance user interface screens for client applications. " + + "The template data returned consists of any or all of:\n" + "\n" + "Field Defaults\n" + "Allowed Value Lists\n" + + "Example Requests:\n" + "\n" + "standinginstructions/template?fromAccountType=2&fromOfficeId=1\n" + "\n" + + "standinginstructions/template?fromAccountType=2&fromOfficeId=1&fromClientId=1&transferType=1\n" + "\n" + + "standinginstructions/template?fromClientId=1&fromAccountType=2&fromAccountId=1&transferType=1") + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StandingInstructionApiResourceSwagger.GetStandingInstructionsTemplateResponse.class))) + public StandingInstructionData template(@BeanParam StandingInstructionSearchParam instructionParam) { + context.authenticatedUser().validateHasReadPermission(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME); + + return standingInstructionReadPlatformService.retrieveTemplate(instructionParam.getFromOfficeId(), + instructionParam.getFromClientId(), instructionParam.getFromAccountId(), instructionParam.getFromAccountType(), + instructionParam.getToOfficeId(), instructionParam.getToClientId(), instructionParam.getToAccountId(), + instructionParam.getToAccountType(), instructionParam.getTransferType()); + } + + @POST + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Create new Standing Instruction", description = "Ability to create new instruction for transfer of monetary funds from one account to another") + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = StandingInstructionCreationRequest.class))) + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StandingInstructionApiResourceSwagger.PostStandingInstructionsResponse.class))) + public CommandProcessingResult create(@Parameter(hidden = true) StandingInstructionCreationRequest creationRequest) { + final CommandWrapper commandRequest = new CommandWrapperBuilder().createStandingInstruction() + .withJson(toApiJsonSerializer.serialize(creationRequest)).build(); + + return commandsSourceWritePlatformService.logCommandSource(commandRequest); + } + + @PUT + @Path("{standingInstructionId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Update Standing Instruction | Delete Standing Instruction", description = "Ability to modify existing instruction for transfer of monetary funds from one account to another.\n" + + "\n" + "PUT https://DomainName/api/v1/standinginstructions/1?command=update\n" + "\n\n" + + "Ability to modify existing instruction for transfer of monetary funds from one account to another.\n" + "\n" + + "PUT https://DomainName/api/v1/standinginstructions/1?command=delete") + @RequestBody(content = @Content(schema = @Schema(implementation = StandingInstructionUpdatesRequest.class))) + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StandingInstructionApiResourceSwagger.PutStandingInstructionsStandingInstructionIdResponse.class))) + public CommandProcessingResult update( + @PathParam("standingInstructionId") @Parameter(description = "standingInstructionId") final Long standingInstructionId, + @Parameter(hidden = true) StandingInstructionUpdatesRequest updatesRequest, + @QueryParam("command") @Parameter(description = "command") final String commandParam) { + + final String serializedUpdatesRequest = toApiJsonSerializer.serialize(updatesRequest); + final CommandWrapper commandRequest = COMMAND_HANDLER_REGISTRY.execute(commandParam, standingInstructionId, + serializedUpdatesRequest, new UnrecognizedQueryParamException("command", commandParam)); + + return commandsSourceWritePlatformService.logCommandSource(commandRequest); + } + + @GET + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "List Standing Instructions", description = "Example Requests:\n" + "\n" + "standinginstructions") + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StandingInstructionApiResourceSwagger.GetStandingInstructionsResponse.class))) + public Page retrieveAll( + @QueryParam("externalId") @Parameter(description = "externalId") final String externalId, + @QueryParam("offset") @Parameter(description = "offset") final Integer offset, + @QueryParam("limit") @Parameter(description = "limit") final Integer limit, + @QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy, + @QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder, + @QueryParam("transferType") @Parameter(description = "transferType") final Integer transferType, + @QueryParam("clientName") @Parameter(description = "clientName") final String clientName, + @QueryParam("clientId") @Parameter(description = "clientId") final Long clientId, + @QueryParam("fromAccountId") @Parameter(description = "fromAccountId") final Long fromAccount, + @QueryParam("fromAccountType") @Parameter(description = "fromAccountType") final Integer fromAccountType) { + + context.authenticatedUser().validateHasReadPermission(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME); + + sqlValidator.validate(orderBy); + sqlValidator.validate(sortOrder); + sqlValidator.validate(externalId); + final SearchParameters searchParameters = SearchParameters.builder().limit(limit).externalId(externalId).offset(offset) + .orderBy(orderBy).sortOrder(sortOrder).build(); + + final LocalDate startDateRange = null; + final LocalDate endDateRange = null; + StandingInstructionDTO standingInstructionDTO = new StandingInstructionDTO(searchParameters, transferType, clientName, clientId, + fromAccount, fromAccountType, startDateRange, endDateRange); + + return standingInstructionReadPlatformService.retrieveAll(standingInstructionDTO); + } + + @GET + @Path("{standingInstructionId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Retrieve Standing Instruction", description = "Example Requests :\n" + "\n" + "standinginstructions/1") + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StandingInstructionApiResourceSwagger.GetStandingInstructionsStandingInstructionIdResponse.class))) + public StandingInstructionData retrieveOne( + @PathParam("standingInstructionId") @Parameter(description = "standingInstructionId") final Long standingInstructionId, + @Context final UriInfo uriInfo, @QueryParam("externalId") @Parameter(description = "externalId") final String externalId, + @QueryParam("offset") @Parameter(description = "offset") final Integer offset, + @QueryParam("limit") @Parameter(description = "limit") final Integer limit, + @QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy, + @QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder) { + + context.authenticatedUser().validateHasReadPermission(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME); + + sqlValidator.validate(orderBy); + sqlValidator.validate(sortOrder); + StandingInstructionData standingInstructionData = standingInstructionReadPlatformService.retrieveOne(standingInstructionId); + final SearchParameters searchParameters = SearchParameters.builder().limit(limit).externalId(externalId).offset(offset) + .orderBy(orderBy).sortOrder(sortOrder).build(); + final Set associationParameters = ApiParameterHelper.extractAssociationsForResponseIfProvided(uriInfo.getQueryParameters()); + if (!associationParameters.isEmpty()) { + if (associationParameters.contains("all")) { + associationParameters.addAll(Arrays.asList("transactions", "template")); + } + if (associationParameters.contains("transactions")) { + Page transfers = accountTransfersReadPlatformService + .retrieveByStandingInstruction(standingInstructionId, searchParameters); + standingInstructionData = StandingInstructionData.withTransferData(standingInstructionData, transfers); + } + if (associationParameters.contains("template")) { + final StandingInstructionData templateData = standingInstructionReadPlatformService.retrieveTemplate( + standingInstructionData.getFromClient().getOfficeId(), standingInstructionData.getFromClient().getId(), + standingInstructionData.getFromAccount().getId(), standingInstructionData.fromAccountTypeEnum().getValue(), + standingInstructionData.getToClient().getOfficeId(), standingInstructionData.getToClient().getId(), + standingInstructionData.getToAccount().getId(), standingInstructionData.toAccountTypeEnum().getValue(), + standingInstructionData.transferTypeEnum().getValue()); + standingInstructionData = StandingInstructionData.withTemplateData(standingInstructionData, templateData); + } + } + + return standingInstructionData; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/data/AccountTransfersDetailDataValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/data/AccountTransfersDetailDataValidator.java index 8c7f876ba..d6dd44145 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/data/AccountTransfersDetailDataValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/data/AccountTransfersDetailDataValidator.java @@ -1,79 +1,79 @@ -/** - * 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.account.data; - -import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromAccountIdParamName; -import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromAccountTypeParamName; -import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromClientIdParamName; -import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromOfficeIdParamName; -import static org.apache.fineract.portfolio.account.AccountDetailConstants.toAccountIdParamName; -import static org.apache.fineract.portfolio.account.AccountDetailConstants.toAccountTypeParamName; -import static org.apache.fineract.portfolio.account.AccountDetailConstants.toClientIdParamName; -import static org.apache.fineract.portfolio.account.AccountDetailConstants.toOfficeIdParamName; - -import com.google.gson.JsonElement; -import org.apache.fineract.infrastructure.core.api.JsonCommand; -import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; -import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -@Component -public class AccountTransfersDetailDataValidator { - - private final FromJsonHelper fromApiJsonHelper; - - @Autowired - public AccountTransfersDetailDataValidator(final FromJsonHelper fromApiJsonHelper) { - this.fromApiJsonHelper = fromApiJsonHelper; - } - - public void validate(final JsonCommand command, final DataValidatorBuilder baseDataValidator) { - - final JsonElement element = command.parsedJson(); - - final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element); - baseDataValidator.reset().parameter(fromOfficeIdParamName).value(fromOfficeId).notNull().integerGreaterThanZero(); - - final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element); - baseDataValidator.reset().parameter(fromClientIdParamName).value(fromClientId).notNull().integerGreaterThanZero(); - - final Long fromAccountId = this.fromApiJsonHelper.extractLongNamed(fromAccountIdParamName, element); - baseDataValidator.reset().parameter(fromAccountIdParamName).value(fromAccountId).notNull().integerGreaterThanZero(); - - final Integer fromAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(fromAccountTypeParamName, element); - baseDataValidator.reset().parameter(fromAccountTypeParamName).value(fromAccountType).notNull() - .isOneOfTheseValues(Integer.valueOf(1), Integer.valueOf(2)); - - final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element); - baseDataValidator.reset().parameter(toOfficeIdParamName).value(toOfficeId).notNull().integerGreaterThanZero(); - - final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element); - baseDataValidator.reset().parameter(toClientIdParamName).value(toClientId).notNull().integerGreaterThanZero(); - - final Long toAccountId = this.fromApiJsonHelper.extractLongNamed(toAccountIdParamName, element); - baseDataValidator.reset().parameter(toAccountIdParamName).value(toAccountId).notNull().integerGreaterThanZero(); - - final Integer toAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(toAccountTypeParamName, element); - baseDataValidator.reset().parameter(toAccountTypeParamName).value(toAccountType).notNull().isOneOfTheseValues(Integer.valueOf(1), - Integer.valueOf(2)); - - } - -} +/** + * 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.account.data; + +import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromAccountIdParamName; +import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromAccountTypeParamName; +import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromClientIdParamName; +import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromOfficeIdParamName; +import static org.apache.fineract.portfolio.account.AccountDetailConstants.toAccountIdParamName; +import static org.apache.fineract.portfolio.account.AccountDetailConstants.toAccountTypeParamName; +import static org.apache.fineract.portfolio.account.AccountDetailConstants.toClientIdParamName; +import static org.apache.fineract.portfolio.account.AccountDetailConstants.toOfficeIdParamName; + +import com.google.gson.JsonElement; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class AccountTransfersDetailDataValidator { + + private final FromJsonHelper fromApiJsonHelper; + + @Autowired + public AccountTransfersDetailDataValidator(final FromJsonHelper fromApiJsonHelper) { + this.fromApiJsonHelper = fromApiJsonHelper; + } + + public void validate(final JsonCommand command, final DataValidatorBuilder baseDataValidator) { + + final JsonElement element = command.parsedJson(); + + final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element); + baseDataValidator.reset().parameter(fromOfficeIdParamName).value(fromOfficeId).notNull().integerGreaterThanZero(); + + final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element); + baseDataValidator.reset().parameter(fromClientIdParamName).value(fromClientId).notNull().integerGreaterThanZero(); + + final Long fromAccountId = this.fromApiJsonHelper.extractLongNamed(fromAccountIdParamName, element); + baseDataValidator.reset().parameter(fromAccountIdParamName).value(fromAccountId).notNull().integerGreaterThanZero(); + + final Integer fromAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(fromAccountTypeParamName, element); + baseDataValidator.reset().parameter(fromAccountTypeParamName).value(fromAccountType).notNull() + .isOneOfTheseValues(Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3)); + + final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element); + baseDataValidator.reset().parameter(toOfficeIdParamName).value(toOfficeId).notNull().integerGreaterThanZero(); + + final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element); + baseDataValidator.reset().parameter(toClientIdParamName).value(toClientId).notNull().integerGreaterThanZero(); + + final Long toAccountId = this.fromApiJsonHelper.extractLongNamed(toAccountIdParamName, element); + baseDataValidator.reset().parameter(toAccountIdParamName).value(toAccountId).notNull().integerGreaterThanZero(); + + final Integer toAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(toAccountTypeParamName, element); + baseDataValidator.reset().parameter(toAccountTypeParamName).value(toAccountType).notNull().isOneOfTheseValues(Integer.valueOf(1), + Integer.valueOf(2), Integer.valueOf(3)); + + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/data/StandingInstructionData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/data/StandingInstructionData.java index 09b61c6b9..45e9f27ca 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/data/StandingInstructionData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/data/StandingInstructionData.java @@ -1,329 +1,359 @@ -/** - * 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.account.data; - -import java.math.BigDecimal; -import java.time.LocalDate; -import java.time.MonthDay; -import java.time.temporal.ChronoField; -import java.util.Collection; -import java.util.Optional; -import lombok.Getter; -import org.apache.fineract.infrastructure.core.data.EnumOptionData; -import org.apache.fineract.infrastructure.core.service.Page; -import org.apache.fineract.organisation.office.data.OfficeData; -import org.apache.fineract.portfolio.account.PortfolioAccountType; -import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType; -import org.apache.fineract.portfolio.account.domain.AccountTransferType; -import org.apache.fineract.portfolio.account.domain.StandingInstructionType; -import org.apache.fineract.portfolio.client.data.ClientData; -import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; -import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; - -/** - * Immutable data object representing a savings account. - */ -@SuppressWarnings("unused") -public final class StandingInstructionData { - - @Getter - private final Long id; - @Getter - private final Long accountDetailId; - @Getter - private final String name; - private final OfficeData fromOffice; - @Getter - private final ClientData fromClient; - private final EnumOptionData fromAccountType; - @Getter - private final PortfolioAccountData fromAccount; - private final OfficeData toOffice; - @Getter - private final ClientData toClient; - private final EnumOptionData toAccountType; - @Getter - private final PortfolioAccountData toAccount; - private final EnumOptionData transferType; - private final EnumOptionData priority; - private final EnumOptionData instructionType; - @Getter - private final EnumOptionData status; - @Getter - private final BigDecimal amount; - @Getter - private final LocalDate validFrom; - private final LocalDate validTill; - private final EnumOptionData recurrenceType; - private final EnumOptionData recurrenceFrequency; - @Getter - private final Integer recurrenceInterval; - private final MonthDay recurrenceOnMonthDay; - private final Page transactions; - - private final Collection fromOfficeOptions; - private final Collection fromClientOptions; - private final Collection fromAccountTypeOptions; - private final Collection fromAccountOptions; - private final Collection toOfficeOptions; - private final Collection toClientOptions; - private final Collection toAccountTypeOptions; - private final Collection toAccountOptions; - private final Collection transferTypeOptions; - private final Collection statusOptions; - private final Collection instructionTypeOptions; - private final Collection priorityOptions; - private final Collection recurrenceTypeOptions; - private final Collection recurrenceFrequencyOptions; - - public static StandingInstructionData template(final Collection fromOfficeOptions, - final Collection fromClientOptions, final Collection fromAccountTypeOptions, - final Collection fromAccountOptions, final Collection toOfficeOptions, - final Collection toClientOptions, final Collection toAccountTypeOptions, - final Collection toAccountOptions, final Collection transferTypeOptions, - final Collection statusOptions, final Collection instructionTypeOptions, - final Collection priorityOptions, final Collection recurrenceTypeOptions, - final Collection recurrenceFrequencyOptions) { - - final Long id = null; - final Long accountDetailId = null; - final String name = null; - final OfficeData fromOffice = null; - final OfficeData toOffice = null; - final ClientData fromClient = null; - final ClientData toClient = null; - final EnumOptionData fromAccountType = null; - final PortfolioAccountData fromAccount = null; - final EnumOptionData toAccountType = null; - final PortfolioAccountData toAccount = null; - final EnumOptionData transferType = null; - final EnumOptionData priority = null; - final EnumOptionData instructionType = null; - final EnumOptionData status = null; - final BigDecimal amount = null; - final LocalDate validFrom = null; - final LocalDate validTill = null; - final EnumOptionData recurrenceType = null; - final EnumOptionData recurrenceFrequency = null; - final Integer recurrenceInterval = null; - final MonthDay recurrenceOnMonthDay = null; - final Page transactions = null; - - return new StandingInstructionData(id, accountDetailId, name, fromOffice, fromClient, fromAccountType, fromAccount, toOffice, - toClient, toAccountType, toAccount, transferType, priority, instructionType, status, amount, validFrom, validTill, - recurrenceType, recurrenceFrequency, recurrenceInterval, recurrenceOnMonthDay, transactions, fromOfficeOptions, - fromClientOptions, fromAccountTypeOptions, fromAccountOptions, toOfficeOptions, toClientOptions, toAccountTypeOptions, - toAccountOptions, transferTypeOptions, statusOptions, instructionTypeOptions, priorityOptions, recurrenceTypeOptions, - recurrenceFrequencyOptions); - } - - public static StandingInstructionData instance(final Long id, final Long accountDetailId, final String name, - final OfficeData fromOffice, final OfficeData toOffice, final ClientData fromClient, final ClientData toClient, - final EnumOptionData fromAccountType, final PortfolioAccountData fromAccount, final EnumOptionData toAccountType, - final PortfolioAccountData toAccount, final EnumOptionData transferType, final EnumOptionData priority, - final EnumOptionData instructionType, final EnumOptionData status, final BigDecimal amount, final LocalDate validFrom, - final LocalDate validTill, final EnumOptionData recurrenceType, final EnumOptionData recurrenceFrequency, - final Integer recurrenceInterval, final MonthDay recurrenceOnMonthDay) { - final Page transactions = null; - final Collection fromOfficeOptions = null; - final Collection fromClientOptions = null; - final Collection fromAccountTypeOptions = null; - final Collection fromAccountOptions = null; - final Collection toOfficeOptions = null; - final Collection toClientOptions = null; - final Collection toAccountTypeOptions = null; - final Collection toAccountOptions = null; - final Collection transferTypeOptions = null; - final Collection statusOptions = null; - final Collection instructionTypeOptions = null; - final Collection priorityOptions = null; - final Collection recurrenceTypeOptions = null; - final Collection recurrenceFrequencyOptions = null; - - return new StandingInstructionData(id, accountDetailId, name, fromOffice, fromClient, fromAccountType, fromAccount, toOffice, - toClient, toAccountType, toAccount, transferType, priority, instructionType, status, amount, validFrom, validTill, - recurrenceType, recurrenceFrequency, recurrenceInterval, recurrenceOnMonthDay, transactions, fromOfficeOptions, - fromClientOptions, fromAccountTypeOptions, fromAccountOptions, toOfficeOptions, toClientOptions, toAccountTypeOptions, - toAccountOptions, transferTypeOptions, statusOptions, instructionTypeOptions, priorityOptions, recurrenceTypeOptions, - recurrenceFrequencyOptions); - } - - public static StandingInstructionData withTemplateData(StandingInstructionData instructionData, StandingInstructionData templateData) { - return new StandingInstructionData(instructionData.id, instructionData.accountDetailId, instructionData.name, - instructionData.fromOffice, instructionData.fromClient, instructionData.fromAccountType, instructionData.fromAccount, - instructionData.toOffice, instructionData.toClient, instructionData.toAccountType, instructionData.toAccount, - instructionData.transferType, instructionData.priority, instructionData.instructionType, instructionData.status, - instructionData.amount, instructionData.validFrom, instructionData.validTill, instructionData.recurrenceType, - instructionData.recurrenceFrequency, instructionData.recurrenceInterval, instructionData.recurrenceOnMonthDay, - instructionData.transactions, templateData.fromOfficeOptions, templateData.fromClientOptions, - templateData.fromAccountTypeOptions, templateData.fromAccountOptions, templateData.toOfficeOptions, - templateData.toClientOptions, templateData.toAccountTypeOptions, templateData.toAccountOptions, - templateData.transferTypeOptions, templateData.statusOptions, templateData.instructionTypeOptions, - templateData.priorityOptions, templateData.recurrenceTypeOptions, templateData.recurrenceFrequencyOptions); - } - - private StandingInstructionData(final Long id, final Long accountDetailId, final String name, final OfficeData fromOffice, - final ClientData fromClient, final EnumOptionData fromAccountType, final PortfolioAccountData fromAccount, - final OfficeData toOffice, final ClientData toClient, final EnumOptionData toAccountType, final PortfolioAccountData toAccount, - final EnumOptionData transferType, final EnumOptionData priority, final EnumOptionData instructionType, - final EnumOptionData status, final BigDecimal amount, final LocalDate validFrom, LocalDate validTill, - final EnumOptionData recurrenceType, final EnumOptionData recurrenceFrequency, final Integer recurrenceInterval, - final MonthDay recurrenceOnMonthDay, final Page transactions, - final Collection fromOfficeOptions, final Collection fromClientOptions, - final Collection fromAccountTypeOptions, final Collection fromAccountOptions, - final Collection toOfficeOptions, final Collection toClientOptions, - final Collection toAccountTypeOptions, final Collection toAccountOptions, - final Collection transferTypeOptions, final Collection statusOptions, - final Collection instructionTypeOptions, final Collection priorityOptions, - final Collection recurrenceTypeOptions, final Collection recurrenceFrequencyOptions) { - this.id = id; - this.accountDetailId = accountDetailId; - this.name = name; - this.fromOffice = fromOffice; - this.fromClient = fromClient; - this.fromAccountType = fromAccountType; - this.fromAccount = fromAccount; - this.toOffice = toOffice; - this.toClient = toClient; - this.toAccountType = toAccountType; - this.toAccount = toAccount; - this.transferType = transferType; - - this.priority = priority; - this.instructionType = instructionType; - this.status = status; - this.amount = amount; - this.validFrom = validFrom; - this.validTill = validTill; - this.recurrenceType = recurrenceType; - this.recurrenceFrequency = recurrenceFrequency; - this.recurrenceInterval = recurrenceInterval; - this.recurrenceOnMonthDay = recurrenceOnMonthDay; - - this.fromOfficeOptions = fromOfficeOptions; - this.fromClientOptions = fromClientOptions; - this.fromAccountTypeOptions = fromAccountTypeOptions; - this.fromAccountOptions = fromAccountOptions; - this.toOfficeOptions = toOfficeOptions; - this.toClientOptions = toClientOptions; - this.toAccountTypeOptions = toAccountTypeOptions; - this.toAccountOptions = toAccountOptions; - this.transferTypeOptions = transferTypeOptions; - this.statusOptions = statusOptions; - this.instructionTypeOptions = instructionTypeOptions; - this.priorityOptions = priorityOptions; - this.recurrenceTypeOptions = recurrenceTypeOptions; - this.recurrenceFrequencyOptions = recurrenceFrequencyOptions; - this.transactions = transactions; - } - - public static StandingInstructionData template(OfficeData fromOffice, ClientData fromClient, EnumOptionData fromAccountType, - PortfolioAccountData fromAccount, LocalDate transferDate, OfficeData toOffice, ClientData toClient, - EnumOptionData toAccountType, PortfolioAccountData toAccount, final Collection fromOfficeOptions, - final Collection fromClientOptions, final Collection fromAccountTypeOptions, - final Collection fromAccountOptions, final Collection toOfficeOptions, - final Collection toClientOptions, final Collection toAccountTypeOptions, - final Collection toAccountOptions, final Collection transferTypeOptions, - final Collection statusOptions, final Collection instructionTypeOptions, - final Collection priorityOptions, final Collection recurrenceTypeOptions, - final Collection recurrenceFrequencyOptions) { - final Long id = null; - final Long accountDetailId = null; - final String name = null; - final EnumOptionData transferType = null; - final EnumOptionData priority = null; - final EnumOptionData instructionType = null; - final EnumOptionData status = null; - final BigDecimal amount = null; - final LocalDate validFrom = null; - final LocalDate validTill = null; - final EnumOptionData recurrenceType = null; - final EnumOptionData recurrenceFrequency = null; - final Integer recurrenceInterval = null; - final MonthDay recurrenceOnMonthDay = null; - final Page transactions = null; - - return new StandingInstructionData(id, accountDetailId, name, fromOffice, fromClient, fromAccountType, fromAccount, toOffice, - toClient, toAccountType, toAccount, transferType, priority, instructionType, status, amount, validFrom, validTill, - recurrenceType, recurrenceFrequency, recurrenceInterval, recurrenceOnMonthDay, transactions, fromOfficeOptions, - fromClientOptions, fromAccountTypeOptions, fromAccountOptions, toOfficeOptions, toClientOptions, toAccountTypeOptions, - toAccountOptions, transferTypeOptions, statusOptions, instructionTypeOptions, priorityOptions, recurrenceTypeOptions, - recurrenceFrequencyOptions); - } - - public static StandingInstructionData withTransferData(StandingInstructionData instructionData, - final Page transactions) { - return new StandingInstructionData(instructionData.id, instructionData.accountDetailId, instructionData.name, - instructionData.fromOffice, instructionData.fromClient, instructionData.fromAccountType, instructionData.fromAccount, - instructionData.toOffice, instructionData.toClient, instructionData.toAccountType, instructionData.toAccount, - instructionData.transferType, instructionData.priority, instructionData.instructionType, instructionData.status, - instructionData.amount, instructionData.validFrom, instructionData.validTill, instructionData.recurrenceType, - instructionData.recurrenceFrequency, instructionData.recurrenceInterval, instructionData.recurrenceOnMonthDay, transactions, - instructionData.fromOfficeOptions, instructionData.fromClientOptions, instructionData.fromAccountTypeOptions, - instructionData.fromAccountOptions, instructionData.toOfficeOptions, instructionData.toClientOptions, - instructionData.toAccountTypeOptions, instructionData.toAccountOptions, instructionData.transferTypeOptions, - instructionData.statusOptions, instructionData.instructionTypeOptions, instructionData.priorityOptions, - instructionData.recurrenceTypeOptions, instructionData.recurrenceFrequencyOptions); - } - - public StandingInstructionType getInstructionType() { - return Optional.ofNullable(this.instructionType).map(e -> StandingInstructionType.fromInt(e.getId().intValue())).orElse(null); - - } - - public AccountTransferRecurrenceType getRecurrenceType() { - return Optional.ofNullable(this.recurrenceType).map(e -> AccountTransferRecurrenceType.fromInt(e.getId().intValue())).orElse(null); - } - - public PeriodFrequencyType getRecurrenceFrequency() { - return Optional.ofNullable(this.recurrenceFrequency).map(e -> PeriodFrequencyType.fromInt(e.getId().intValue())).orElse(null); - } - - public PortfolioAccountType getFromAccountType() { - return Optional.ofNullable(this.fromAccountType).map(e -> PortfolioAccountType.fromInt(e.getId().intValue())).orElse(null); - } - - public PortfolioAccountType getToAccountType() { - return Optional.ofNullable(this.toAccountType).map(e -> PortfolioAccountType.fromInt(e.getId().intValue())).orElse(null); - } - - public AccountTransferType getTransferType() { - return Optional.ofNullable(this.transferType).map(e -> AccountTransferType.fromInt(e.getId().intValue())).orElse(null); - } - - public Integer getRecurrenceOnDay() { - return Optional.ofNullable(this.recurrenceOnMonthDay).map(monthDay -> monthDay.get(ChronoField.DAY_OF_MONTH)).orElse(0); - } - - public Integer getRecurrenceOnMonth() { - return Optional.ofNullable(this.recurrenceOnMonthDay).map(monthDay -> monthDay.get(ChronoField.MONTH_OF_YEAR)).orElse(0); - } - - public Integer toTransferType() { - Integer transferType = null; - AccountTransferType accountTransferType = getTransferType(); - if (accountTransferType.isChargePayment()) { - transferType = LoanTransactionType.CHARGE_PAYMENT.getValue(); - } else if (accountTransferType.isLoanRepayment()) { - transferType = LoanTransactionType.REPAYMENT.getValue(); - } - return transferType; - } - -} +/** + * 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.account.data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.MonthDay; +import java.time.temporal.ChronoField; +import java.util.Collection; +import java.util.Optional; +import lombok.Getter; +import org.apache.fineract.infrastructure.core.data.EnumOptionData; +import org.apache.fineract.infrastructure.core.service.Page; +import org.apache.fineract.organisation.office.data.OfficeData; +import org.apache.fineract.portfolio.account.PortfolioAccountType; +import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType; +import org.apache.fineract.portfolio.account.domain.AccountTransferType; +import org.apache.fineract.portfolio.account.domain.StandingInstructionType; +import org.apache.fineract.portfolio.client.data.ClientData; +import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; + +/** + * Immutable data object representing a savings account. + */ +@SuppressWarnings("unused") +public final class StandingInstructionData { + + @Getter + private final Long id; + @Getter + private final Long accountDetailId; + @Getter + private final String name; + @Getter + private final OfficeData fromOffice; + @Getter + private final ClientData fromClient; + @Getter + private final EnumOptionData fromAccountType; + @Getter + private final PortfolioAccountData fromAccount; + @Getter + private final OfficeData toOffice; + @Getter + private final ClientData toClient; + @Getter + private final EnumOptionData toAccountType; + @Getter + private final PortfolioAccountData toAccount; + @Getter + private final EnumOptionData transferType; + @Getter + private final EnumOptionData priority; + @Getter + private final EnumOptionData instructionType; + @Getter + private final EnumOptionData status; + @Getter + private final BigDecimal amount; + @Getter + private final LocalDate validFrom; + @Getter + private final LocalDate validTill; + @Getter + private final EnumOptionData recurrenceType; + @Getter + private final EnumOptionData recurrenceFrequency; + @Getter + private final Integer recurrenceInterval; + @Getter + private final MonthDay recurrenceOnMonthDay; + @Getter + private final Page transactions; + + @Getter + private final Collection fromOfficeOptions; + @Getter + private final Collection fromClientOptions; + @Getter + private final Collection fromAccountTypeOptions; + @Getter + private final Collection fromAccountOptions; + @Getter + private final Collection toOfficeOptions; + @Getter + private final Collection toClientOptions; + @Getter + private final Collection toAccountTypeOptions; + @Getter + private final Collection toAccountOptions; + @Getter + private final Collection transferTypeOptions; + @Getter + private final Collection statusOptions; + @Getter + private final Collection instructionTypeOptions; + @Getter + private final Collection priorityOptions; + @Getter + private final Collection recurrenceTypeOptions; + @Getter + private final Collection recurrenceFrequencyOptions; + + public static StandingInstructionData template(final Collection fromOfficeOptions, + final Collection fromClientOptions, final Collection fromAccountTypeOptions, + final Collection fromAccountOptions, final Collection toOfficeOptions, + final Collection toClientOptions, final Collection toAccountTypeOptions, + final Collection toAccountOptions, final Collection transferTypeOptions, + final Collection statusOptions, final Collection instructionTypeOptions, + final Collection priorityOptions, final Collection recurrenceTypeOptions, + final Collection recurrenceFrequencyOptions) { + + final Long id = null; + final Long accountDetailId = null; + final String name = null; + final OfficeData fromOffice = null; + final OfficeData toOffice = null; + final ClientData fromClient = null; + final ClientData toClient = null; + final EnumOptionData fromAccountType = null; + final PortfolioAccountData fromAccount = null; + final EnumOptionData toAccountType = null; + final PortfolioAccountData toAccount = null; + final EnumOptionData transferType = null; + final EnumOptionData priority = null; + final EnumOptionData instructionType = null; + final EnumOptionData status = null; + final BigDecimal amount = null; + final LocalDate validFrom = null; + final LocalDate validTill = null; + final EnumOptionData recurrenceType = null; + final EnumOptionData recurrenceFrequency = null; + final Integer recurrenceInterval = null; + final MonthDay recurrenceOnMonthDay = null; + final Page transactions = null; + + return new StandingInstructionData(id, accountDetailId, name, fromOffice, fromClient, fromAccountType, fromAccount, toOffice, + toClient, toAccountType, toAccount, transferType, priority, instructionType, status, amount, validFrom, validTill, + recurrenceType, recurrenceFrequency, recurrenceInterval, recurrenceOnMonthDay, transactions, fromOfficeOptions, + fromClientOptions, fromAccountTypeOptions, fromAccountOptions, toOfficeOptions, toClientOptions, toAccountTypeOptions, + toAccountOptions, transferTypeOptions, statusOptions, instructionTypeOptions, priorityOptions, recurrenceTypeOptions, + recurrenceFrequencyOptions); + } + + public static StandingInstructionData instance(final Long id, final Long accountDetailId, final String name, + final OfficeData fromOffice, final OfficeData toOffice, final ClientData fromClient, final ClientData toClient, + final EnumOptionData fromAccountType, final PortfolioAccountData fromAccount, final EnumOptionData toAccountType, + final PortfolioAccountData toAccount, final EnumOptionData transferType, final EnumOptionData priority, + final EnumOptionData instructionType, final EnumOptionData status, final BigDecimal amount, final LocalDate validFrom, + final LocalDate validTill, final EnumOptionData recurrenceType, final EnumOptionData recurrenceFrequency, + final Integer recurrenceInterval, final MonthDay recurrenceOnMonthDay) { + final Page transactions = null; + final Collection fromOfficeOptions = null; + final Collection fromClientOptions = null; + final Collection fromAccountTypeOptions = null; + final Collection fromAccountOptions = null; + final Collection toOfficeOptions = null; + final Collection toClientOptions = null; + final Collection toAccountTypeOptions = null; + final Collection toAccountOptions = null; + final Collection transferTypeOptions = null; + final Collection statusOptions = null; + final Collection instructionTypeOptions = null; + final Collection priorityOptions = null; + final Collection recurrenceTypeOptions = null; + final Collection recurrenceFrequencyOptions = null; + + return new StandingInstructionData(id, accountDetailId, name, fromOffice, fromClient, fromAccountType, fromAccount, toOffice, + toClient, toAccountType, toAccount, transferType, priority, instructionType, status, amount, validFrom, validTill, + recurrenceType, recurrenceFrequency, recurrenceInterval, recurrenceOnMonthDay, transactions, fromOfficeOptions, + fromClientOptions, fromAccountTypeOptions, fromAccountOptions, toOfficeOptions, toClientOptions, toAccountTypeOptions, + toAccountOptions, transferTypeOptions, statusOptions, instructionTypeOptions, priorityOptions, recurrenceTypeOptions, + recurrenceFrequencyOptions); + } + + public static StandingInstructionData withTemplateData(StandingInstructionData instructionData, StandingInstructionData templateData) { + return new StandingInstructionData(instructionData.id, instructionData.accountDetailId, instructionData.name, + instructionData.fromOffice, instructionData.fromClient, instructionData.fromAccountType, instructionData.fromAccount, + instructionData.toOffice, instructionData.toClient, instructionData.toAccountType, instructionData.toAccount, + instructionData.transferType, instructionData.priority, instructionData.instructionType, instructionData.status, + instructionData.amount, instructionData.validFrom, instructionData.validTill, instructionData.recurrenceType, + instructionData.recurrenceFrequency, instructionData.recurrenceInterval, instructionData.recurrenceOnMonthDay, + instructionData.transactions, templateData.fromOfficeOptions, templateData.fromClientOptions, + templateData.fromAccountTypeOptions, templateData.fromAccountOptions, templateData.toOfficeOptions, + templateData.toClientOptions, templateData.toAccountTypeOptions, templateData.toAccountOptions, + templateData.transferTypeOptions, templateData.statusOptions, templateData.instructionTypeOptions, + templateData.priorityOptions, templateData.recurrenceTypeOptions, templateData.recurrenceFrequencyOptions); + } + + private StandingInstructionData(final Long id, final Long accountDetailId, final String name, final OfficeData fromOffice, + final ClientData fromClient, final EnumOptionData fromAccountType, final PortfolioAccountData fromAccount, + final OfficeData toOffice, final ClientData toClient, final EnumOptionData toAccountType, final PortfolioAccountData toAccount, + final EnumOptionData transferType, final EnumOptionData priority, final EnumOptionData instructionType, + final EnumOptionData status, final BigDecimal amount, final LocalDate validFrom, LocalDate validTill, + final EnumOptionData recurrenceType, final EnumOptionData recurrenceFrequency, final Integer recurrenceInterval, + final MonthDay recurrenceOnMonthDay, final Page transactions, + final Collection fromOfficeOptions, final Collection fromClientOptions, + final Collection fromAccountTypeOptions, final Collection fromAccountOptions, + final Collection toOfficeOptions, final Collection toClientOptions, + final Collection toAccountTypeOptions, final Collection toAccountOptions, + final Collection transferTypeOptions, final Collection statusOptions, + final Collection instructionTypeOptions, final Collection priorityOptions, + final Collection recurrenceTypeOptions, final Collection recurrenceFrequencyOptions) { + this.id = id; + this.accountDetailId = accountDetailId; + this.name = name; + this.fromOffice = fromOffice; + this.fromClient = fromClient; + this.fromAccountType = fromAccountType; + this.fromAccount = fromAccount; + this.toOffice = toOffice; + this.toClient = toClient; + this.toAccountType = toAccountType; + this.toAccount = toAccount; + this.transferType = transferType; + + this.priority = priority; + this.instructionType = instructionType; + this.status = status; + this.amount = amount; + this.validFrom = validFrom; + this.validTill = validTill; + this.recurrenceType = recurrenceType; + this.recurrenceFrequency = recurrenceFrequency; + this.recurrenceInterval = recurrenceInterval; + this.recurrenceOnMonthDay = recurrenceOnMonthDay; + + this.fromOfficeOptions = fromOfficeOptions; + this.fromClientOptions = fromClientOptions; + this.fromAccountTypeOptions = fromAccountTypeOptions; + this.fromAccountOptions = fromAccountOptions; + this.toOfficeOptions = toOfficeOptions; + this.toClientOptions = toClientOptions; + this.toAccountTypeOptions = toAccountTypeOptions; + this.toAccountOptions = toAccountOptions; + this.transferTypeOptions = transferTypeOptions; + this.statusOptions = statusOptions; + this.instructionTypeOptions = instructionTypeOptions; + this.priorityOptions = priorityOptions; + this.recurrenceTypeOptions = recurrenceTypeOptions; + this.recurrenceFrequencyOptions = recurrenceFrequencyOptions; + this.transactions = transactions; + } + + public static StandingInstructionData template(OfficeData fromOffice, ClientData fromClient, EnumOptionData fromAccountType, + PortfolioAccountData fromAccount, LocalDate transferDate, OfficeData toOffice, ClientData toClient, + EnumOptionData toAccountType, PortfolioAccountData toAccount, final Collection fromOfficeOptions, + final Collection fromClientOptions, final Collection fromAccountTypeOptions, + final Collection fromAccountOptions, final Collection toOfficeOptions, + final Collection toClientOptions, final Collection toAccountTypeOptions, + final Collection toAccountOptions, final Collection transferTypeOptions, + final Collection statusOptions, final Collection instructionTypeOptions, + final Collection priorityOptions, final Collection recurrenceTypeOptions, + final Collection recurrenceFrequencyOptions) { + final Long id = null; + final Long accountDetailId = null; + final String name = null; + final EnumOptionData transferType = null; + final EnumOptionData priority = null; + final EnumOptionData instructionType = null; + final EnumOptionData status = null; + final BigDecimal amount = null; + final LocalDate validFrom = null; + final LocalDate validTill = null; + final EnumOptionData recurrenceType = null; + final EnumOptionData recurrenceFrequency = null; + final Integer recurrenceInterval = null; + final MonthDay recurrenceOnMonthDay = null; + final Page transactions = null; + + return new StandingInstructionData(id, accountDetailId, name, fromOffice, fromClient, fromAccountType, fromAccount, toOffice, + toClient, toAccountType, toAccount, transferType, priority, instructionType, status, amount, validFrom, validTill, + recurrenceType, recurrenceFrequency, recurrenceInterval, recurrenceOnMonthDay, transactions, fromOfficeOptions, + fromClientOptions, fromAccountTypeOptions, fromAccountOptions, toOfficeOptions, toClientOptions, toAccountTypeOptions, + toAccountOptions, transferTypeOptions, statusOptions, instructionTypeOptions, priorityOptions, recurrenceTypeOptions, + recurrenceFrequencyOptions); + } + + public static StandingInstructionData withTransferData(StandingInstructionData instructionData, + final Page transactions) { + return new StandingInstructionData(instructionData.id, instructionData.accountDetailId, instructionData.name, + instructionData.fromOffice, instructionData.fromClient, instructionData.fromAccountType, instructionData.fromAccount, + instructionData.toOffice, instructionData.toClient, instructionData.toAccountType, instructionData.toAccount, + instructionData.transferType, instructionData.priority, instructionData.instructionType, instructionData.status, + instructionData.amount, instructionData.validFrom, instructionData.validTill, instructionData.recurrenceType, + instructionData.recurrenceFrequency, instructionData.recurrenceInterval, instructionData.recurrenceOnMonthDay, transactions, + instructionData.fromOfficeOptions, instructionData.fromClientOptions, instructionData.fromAccountTypeOptions, + instructionData.fromAccountOptions, instructionData.toOfficeOptions, instructionData.toClientOptions, + instructionData.toAccountTypeOptions, instructionData.toAccountOptions, instructionData.transferTypeOptions, + instructionData.statusOptions, instructionData.instructionTypeOptions, instructionData.priorityOptions, + instructionData.recurrenceTypeOptions, instructionData.recurrenceFrequencyOptions); + } + + /** + * Returns the domain enum for instruction type — used internally by business logic. + * NOTE: renamed from getInstructionType() to avoid overriding Lombok @Getter (which must + * return EnumOptionData so Jackson serializes it as an object, not a plain string). + */ + public StandingInstructionType instructionTypeEnum() { + return Optional.ofNullable(this.instructionType).map(e -> StandingInstructionType.fromInt(e.getId().intValue())).orElse(null); + } + + public AccountTransferRecurrenceType recurrenceTypeEnum() { + return Optional.ofNullable(this.recurrenceType).map(e -> AccountTransferRecurrenceType.fromInt(e.getId().intValue())).orElse(null); + } + + public PeriodFrequencyType recurrenceFrequencyEnum() { + return Optional.ofNullable(this.recurrenceFrequency).map(e -> PeriodFrequencyType.fromInt(e.getId().intValue())).orElse(null); + } + + public PortfolioAccountType fromAccountTypeEnum() { + return Optional.ofNullable(this.fromAccountType).map(e -> PortfolioAccountType.fromInt(e.getId().intValue())).orElse(null); + } + + public PortfolioAccountType toAccountTypeEnum() { + return Optional.ofNullable(this.toAccountType).map(e -> PortfolioAccountType.fromInt(e.getId().intValue())).orElse(null); + } + + public AccountTransferType transferTypeEnum() { + return Optional.ofNullable(this.transferType).map(e -> AccountTransferType.fromInt(e.getId().intValue())).orElse(null); + } + + public Integer getRecurrenceOnDay() { + return Optional.ofNullable(this.recurrenceOnMonthDay).map(monthDay -> monthDay.get(ChronoField.DAY_OF_MONTH)).orElse(0); + } + + public Integer getRecurrenceOnMonth() { + return Optional.ofNullable(this.recurrenceOnMonthDay).map(monthDay -> monthDay.get(ChronoField.MONTH_OF_YEAR)).orElse(0); + } + + public Integer toTransferType() { + Integer transferType = null; + AccountTransferType accountTransferType = transferTypeEnum(); + if (accountTransferType != null && accountTransferType.isChargePayment()) { + transferType = LoanTransactionType.CHARGE_PAYMENT.getValue(); + } else if (accountTransferType != null && accountTransferType.isLoanRepayment()) { + transferType = LoanTransactionType.REPAYMENT.getValue(); + } + return transferType; + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/data/StandingInstructionDataValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/data/StandingInstructionDataValidator.java index 552c2fc59..93fc04129 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/data/StandingInstructionDataValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/data/StandingInstructionDataValidator.java @@ -1,294 +1,296 @@ -/** - * 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.account.data; - -import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromAccountTypeParamName; -import static org.apache.fineract.portfolio.account.AccountDetailConstants.toAccountTypeParamName; -import static org.apache.fineract.portfolio.account.AccountDetailConstants.transferTypeParamName; - -import com.google.gson.JsonElement; -import com.google.gson.reflect.TypeToken; -import java.lang.reflect.Type; -import java.math.BigDecimal; -import java.time.LocalDate; -import java.time.MonthDay; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import org.apache.commons.lang3.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.exception.InvalidJsonException; -import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; -import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; -import org.apache.fineract.portfolio.account.AccountDetailConstants; -import org.apache.fineract.portfolio.account.PortfolioAccountType; -import org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants; -import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType; -import org.apache.fineract.portfolio.account.domain.AccountTransferType; -import org.apache.fineract.portfolio.account.domain.StandingInstructionType; -import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -@Component -public class StandingInstructionDataValidator { - - private final FromJsonHelper fromApiJsonHelper; - private final AccountTransfersDetailDataValidator accountTransfersDetailDataValidator; - private static final Set CREATE_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(AccountDetailConstants.localeParamName, - AccountDetailConstants.dateFormatParamName, AccountDetailConstants.fromOfficeIdParamName, - AccountDetailConstants.fromClientIdParamName, AccountDetailConstants.fromAccountTypeParamName, - AccountDetailConstants.fromAccountIdParamName, AccountDetailConstants.toOfficeIdParamName, - AccountDetailConstants.toClientIdParamName, AccountDetailConstants.toAccountTypeParamName, - AccountDetailConstants.toAccountIdParamName, AccountDetailConstants.transferTypeParamName, - StandingInstructionApiConstants.priorityParamName, StandingInstructionApiConstants.instructionTypeParamName, - StandingInstructionApiConstants.statusParamName, StandingInstructionApiConstants.amountParamName, - StandingInstructionApiConstants.validFromParamName, StandingInstructionApiConstants.validTillParamName, - StandingInstructionApiConstants.recurrenceTypeParamName, StandingInstructionApiConstants.recurrenceFrequencyParamName, - StandingInstructionApiConstants.recurrenceIntervalParamName, StandingInstructionApiConstants.recurrenceOnMonthDayParamName, - StandingInstructionApiConstants.nameParamName, StandingInstructionApiConstants.monthDayFormatParamName)); - - private static final Set UPDATE_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(AccountDetailConstants.localeParamName, - AccountDetailConstants.dateFormatParamName, StandingInstructionApiConstants.priorityParamName, - StandingInstructionApiConstants.instructionTypeParamName, StandingInstructionApiConstants.statusParamName, - StandingInstructionApiConstants.amountParamName, StandingInstructionApiConstants.validFromParamName, - StandingInstructionApiConstants.validTillParamName, StandingInstructionApiConstants.recurrenceTypeParamName, - StandingInstructionApiConstants.recurrenceFrequencyParamName, StandingInstructionApiConstants.recurrenceIntervalParamName, - StandingInstructionApiConstants.recurrenceOnMonthDayParamName, StandingInstructionApiConstants.monthDayFormatParamName)); - - @Autowired - public StandingInstructionDataValidator(final FromJsonHelper fromApiJsonHelper, - final AccountTransfersDetailDataValidator accountTransfersDetailDataValidator) { - this.fromApiJsonHelper = fromApiJsonHelper; - this.accountTransfersDetailDataValidator = accountTransfersDetailDataValidator; - } - - public void validateForCreate(final JsonCommand command) { - final String json = command.json(); - - if (StringUtils.isBlank(json)) { - throw new InvalidJsonException(); - } - - final Type typeOfMap = new TypeToken>() {}.getType(); - this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, CREATE_REQUEST_DATA_PARAMETERS); - - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME); - this.accountTransfersDetailDataValidator.validate(command, baseDataValidator); - - final JsonElement element = command.parsedJson(); - - final Integer status = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.statusParamName, element, - Locale.getDefault()); - baseDataValidator.reset().parameter(StandingInstructionApiConstants.statusParamName).value(status).notNull().inMinMaxRange(1, 2); - - final LocalDate validFrom = this.fromApiJsonHelper.extractLocalDateNamed(StandingInstructionApiConstants.validFromParamName, - element); - baseDataValidator.reset().parameter(StandingInstructionApiConstants.validFromParamName).value(validFrom).notNull(); - - final LocalDate validTill = this.fromApiJsonHelper.extractLocalDateNamed(StandingInstructionApiConstants.validTillParamName, - element); - baseDataValidator.reset().parameter(StandingInstructionApiConstants.validTillParamName).value(validTill) - .validateDateAfter(validFrom); - - final BigDecimal transferAmount = this.fromApiJsonHelper - .extractBigDecimalWithLocaleNamed(StandingInstructionApiConstants.amountParamName, element); - baseDataValidator.reset().parameter(StandingInstructionApiConstants.amountParamName).value(transferAmount).positiveAmount(); - - final Integer transferType = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault()); - baseDataValidator.reset().parameter(transferTypeParamName).value(transferType).notNull().inMinMaxRange(1, 3); - - final Integer priority = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.priorityParamName, element, - Locale.getDefault()); - baseDataValidator.reset().parameter(StandingInstructionApiConstants.priorityParamName).value(priority).notNull().inMinMaxRange(1, - 4); - - final Integer standingInstructionType = this.fromApiJsonHelper - .extractIntegerNamed(StandingInstructionApiConstants.instructionTypeParamName, element, Locale.getDefault()); - baseDataValidator.reset().parameter(StandingInstructionApiConstants.instructionTypeParamName).value(standingInstructionType) - .notNull().inMinMaxRange(1, 2); - - final Integer recurrenceType = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.recurrenceTypeParamName, - element, Locale.getDefault()); - baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceTypeParamName).value(recurrenceType).notNull() - .inMinMaxRange(1, 2); - boolean isPeriodic = false; - if (recurrenceType != null) { - isPeriodic = AccountTransferRecurrenceType.fromInt(recurrenceType).isPeriodicRecurrence(); - } - - final Integer recurrenceFrequency = this.fromApiJsonHelper - .extractIntegerNamed(StandingInstructionApiConstants.recurrenceFrequencyParamName, element, Locale.getDefault()); - baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceFrequencyParamName).value(recurrenceFrequency) - .inMinMaxRange(0, 3); - - if (recurrenceFrequency != null) { - PeriodFrequencyType frequencyType = PeriodFrequencyType.fromInt(recurrenceFrequency); - if (frequencyType.isMonthly() || frequencyType.isYearly()) { - final MonthDay monthDay = this.fromApiJsonHelper - .extractMonthDayNamed(StandingInstructionApiConstants.recurrenceOnMonthDayParamName, element); - baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceOnMonthDayParamName).value(monthDay) - .notNull(); - } - } - - final Integer recurrenceInterval = this.fromApiJsonHelper - .extractIntegerNamed(StandingInstructionApiConstants.recurrenceIntervalParamName, element, Locale.getDefault()); - if (isPeriodic) { - baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceIntervalParamName).value(recurrenceInterval) - .notNull(); - baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceFrequencyParamName).value(recurrenceFrequency) - .notNull(); - } - baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceIntervalParamName).value(recurrenceInterval) - .integerGreaterThanZero(); - - final String name = this.fromApiJsonHelper.extractStringNamed(StandingInstructionApiConstants.nameParamName, element); - baseDataValidator.reset().parameter(StandingInstructionApiConstants.nameParamName).value(name).notNull(); - - final Integer toAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(toAccountTypeParamName, element); - if (toAccountType != null && PortfolioAccountType.fromInt(toAccountType).isSavingsAccount()) { - baseDataValidator.reset().parameter(StandingInstructionApiConstants.instructionTypeParamName).value(standingInstructionType) - .notNull().inMinMaxRange(1, 1); - baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceTypeParamName).value(recurrenceType).notNull() - .inMinMaxRange(1, 1); - - } - if (standingInstructionType != null && StandingInstructionType.fromInt(standingInstructionType).isFixedAmoutTransfer()) { - baseDataValidator.reset().parameter(StandingInstructionApiConstants.amountParamName).value(transferAmount).notNull(); - } - - String errorCode = null; - AccountTransferType accountTransferType = AccountTransferType.fromInt(transferType); - final Integer fromAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(fromAccountTypeParamName, element); - if (fromAccountType != null && toAccountType != null) { - PortfolioAccountType fromPortfolioAccountType = PortfolioAccountType.fromInt(fromAccountType); - PortfolioAccountType toPortfolioAccountType = PortfolioAccountType.fromInt(toAccountType); - if (accountTransferType.isAccountTransfer() - && (fromPortfolioAccountType.isLoanAccount() || toPortfolioAccountType.isLoanAccount())) { - errorCode = "not.account.transfer"; - } else if (accountTransferType.isLoanRepayment() - && (fromPortfolioAccountType.isLoanAccount() || toPortfolioAccountType.isSavingsAccount())) { - errorCode = "not.loan.repayment"; - } - if (errorCode != null) { - baseDataValidator.reset().parameter(transferTypeParamName).failWithCode(errorCode); - } - } - - throwExceptionIfValidationWarningsExist(dataValidationErrors); - } - - public void validateForUpdate(final JsonCommand command) { - final String json = command.json(); - - if (StringUtils.isBlank(json)) { - throw new InvalidJsonException(); - } - - final Type typeOfMap = new TypeToken>() {}.getType(); - this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, UPDATE_REQUEST_DATA_PARAMETERS); - - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME); - - final JsonElement element = command.parsedJson(); - if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.validFromParamName, element)) { - final LocalDate validFrom = this.fromApiJsonHelper.extractLocalDateNamed(StandingInstructionApiConstants.validFromParamName, - element); - baseDataValidator.reset().parameter(StandingInstructionApiConstants.validFromParamName).value(validFrom).notNull(); - } - - if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.validTillParamName, element)) { - final LocalDate validTill = this.fromApiJsonHelper.extractLocalDateNamed(StandingInstructionApiConstants.validTillParamName, - element); - baseDataValidator.reset().parameter(StandingInstructionApiConstants.validTillParamName).value(validTill).notNull(); - } - - if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.amountParamName, element)) { - final BigDecimal transferAmount = this.fromApiJsonHelper - .extractBigDecimalWithLocaleNamed(StandingInstructionApiConstants.amountParamName, element); - baseDataValidator.reset().parameter(StandingInstructionApiConstants.amountParamName).value(transferAmount).positiveAmount(); - } - - if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.statusParamName, element)) { - final Integer status = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.statusParamName, element, - Locale.getDefault()); - baseDataValidator.reset().parameter(StandingInstructionApiConstants.statusParamName).value(status).notNull().inMinMaxRange(1, - 2); - } - - if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.priorityParamName, element)) { - final Integer priority = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.priorityParamName, element, - Locale.getDefault()); - baseDataValidator.reset().parameter(StandingInstructionApiConstants.priorityParamName).value(priority).notNull() - .inMinMaxRange(1, 4); - } - - if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.instructionTypeParamName, element)) { - final Integer standingInstructionType = this.fromApiJsonHelper - .extractIntegerNamed(StandingInstructionApiConstants.instructionTypeParamName, element, Locale.getDefault()); - baseDataValidator.reset().parameter(StandingInstructionApiConstants.instructionTypeParamName).value(standingInstructionType) - .notNull().inMinMaxRange(1, 2); - } - - if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.recurrenceTypeParamName, element)) { - final Integer recurrenceType = this.fromApiJsonHelper - .extractIntegerNamed(StandingInstructionApiConstants.recurrenceTypeParamName, element, Locale.getDefault()); - baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceTypeParamName).value(recurrenceType).notNull() - .inMinMaxRange(1, 2); - } - - if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.recurrenceFrequencyParamName, element)) { - final Integer recurrenceFrequency = this.fromApiJsonHelper - .extractIntegerNamed(StandingInstructionApiConstants.recurrenceFrequencyParamName, element, Locale.getDefault()); - baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceFrequencyParamName).value(recurrenceFrequency) - .inMinMaxRange(0, 3); - } - - if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.recurrenceIntervalParamName, element)) { - final Integer recurrenceInterval = this.fromApiJsonHelper - .extractIntegerNamed(StandingInstructionApiConstants.recurrenceIntervalParamName, element, Locale.getDefault()); - baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceIntervalParamName).value(recurrenceInterval) - .integerGreaterThanZero(); - } - - if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.nameParamName, element)) { - final String name = this.fromApiJsonHelper.extractStringNamed(StandingInstructionApiConstants.nameParamName, element); - baseDataValidator.reset().parameter(StandingInstructionApiConstants.nameParamName).value(name).notNull(); - } - - throwExceptionIfValidationWarningsExist(dataValidationErrors); - } - - private void throwExceptionIfValidationWarningsExist(final List dataValidationErrors) { - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } -} +/** + * 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.account.data; + +import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromAccountTypeParamName; +import static org.apache.fineract.portfolio.account.AccountDetailConstants.toAccountTypeParamName; +import static org.apache.fineract.portfolio.account.AccountDetailConstants.transferTypeParamName; + +import com.google.gson.JsonElement; +import com.google.gson.reflect.TypeToken; +import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.MonthDay; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import org.apache.commons.lang3.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.exception.InvalidJsonException; +import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.portfolio.account.AccountDetailConstants; +import org.apache.fineract.portfolio.account.PortfolioAccountType; +import org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants; +import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType; +import org.apache.fineract.portfolio.account.domain.AccountTransferType; +import org.apache.fineract.portfolio.account.domain.StandingInstructionType; +import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class StandingInstructionDataValidator { + + private final FromJsonHelper fromApiJsonHelper; + private final AccountTransfersDetailDataValidator accountTransfersDetailDataValidator; + private static final Set CREATE_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(AccountDetailConstants.localeParamName, + AccountDetailConstants.dateFormatParamName, AccountDetailConstants.fromOfficeIdParamName, + AccountDetailConstants.fromClientIdParamName, AccountDetailConstants.fromAccountTypeParamName, + AccountDetailConstants.fromAccountIdParamName, AccountDetailConstants.toOfficeIdParamName, + AccountDetailConstants.toClientIdParamName, AccountDetailConstants.toAccountTypeParamName, + AccountDetailConstants.toAccountIdParamName, AccountDetailConstants.transferTypeParamName, + StandingInstructionApiConstants.priorityParamName, StandingInstructionApiConstants.instructionTypeParamName, + StandingInstructionApiConstants.statusParamName, StandingInstructionApiConstants.amountParamName, + StandingInstructionApiConstants.validFromParamName, StandingInstructionApiConstants.validTillParamName, + StandingInstructionApiConstants.recurrenceTypeParamName, StandingInstructionApiConstants.recurrenceFrequencyParamName, + StandingInstructionApiConstants.recurrenceIntervalParamName, StandingInstructionApiConstants.recurrenceOnMonthDayParamName, + StandingInstructionApiConstants.nameParamName, StandingInstructionApiConstants.monthDayFormatParamName)); + + private static final Set UPDATE_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(AccountDetailConstants.localeParamName, + AccountDetailConstants.dateFormatParamName, StandingInstructionApiConstants.priorityParamName, + StandingInstructionApiConstants.instructionTypeParamName, StandingInstructionApiConstants.statusParamName, + StandingInstructionApiConstants.amountParamName, StandingInstructionApiConstants.validFromParamName, + StandingInstructionApiConstants.validTillParamName, StandingInstructionApiConstants.recurrenceTypeParamName, + StandingInstructionApiConstants.recurrenceFrequencyParamName, StandingInstructionApiConstants.recurrenceIntervalParamName, + StandingInstructionApiConstants.recurrenceOnMonthDayParamName, StandingInstructionApiConstants.monthDayFormatParamName)); + + @Autowired + public StandingInstructionDataValidator(final FromJsonHelper fromApiJsonHelper, + final AccountTransfersDetailDataValidator accountTransfersDetailDataValidator) { + this.fromApiJsonHelper = fromApiJsonHelper; + this.accountTransfersDetailDataValidator = accountTransfersDetailDataValidator; + } + + public void validateForCreate(final JsonCommand command) { + final String json = command.json(); + + if (StringUtils.isBlank(json)) { + throw new InvalidJsonException(); + } + + final Type typeOfMap = new TypeToken>() {}.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, CREATE_REQUEST_DATA_PARAMETERS); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME); + this.accountTransfersDetailDataValidator.validate(command, baseDataValidator); + + final JsonElement element = command.parsedJson(); + + final Integer status = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.statusParamName, element, + Locale.getDefault()); + baseDataValidator.reset().parameter(StandingInstructionApiConstants.statusParamName).value(status).notNull().inMinMaxRange(1, 2); + + final LocalDate validFrom = this.fromApiJsonHelper.extractLocalDateNamed(StandingInstructionApiConstants.validFromParamName, + element); + baseDataValidator.reset().parameter(StandingInstructionApiConstants.validFromParamName).value(validFrom).notNull(); + + final LocalDate validTill = this.fromApiJsonHelper.extractLocalDateNamed(StandingInstructionApiConstants.validTillParamName, + element); + baseDataValidator.reset().parameter(StandingInstructionApiConstants.validTillParamName).value(validTill) + .validateDateAfter(validFrom); + + final BigDecimal transferAmount = this.fromApiJsonHelper + .extractBigDecimalWithLocaleNamed(StandingInstructionApiConstants.amountParamName, element); + baseDataValidator.reset().parameter(StandingInstructionApiConstants.amountParamName).value(transferAmount).positiveAmount(); + + final Integer transferType = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault()); + baseDataValidator.reset().parameter(transferTypeParamName).value(transferType).notNull().inMinMaxRange(1, 3); + + final Integer priority = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.priorityParamName, element, + Locale.getDefault()); + baseDataValidator.reset().parameter(StandingInstructionApiConstants.priorityParamName).value(priority).notNull().inMinMaxRange(1, + 4); + + final Integer standingInstructionType = this.fromApiJsonHelper + .extractIntegerNamed(StandingInstructionApiConstants.instructionTypeParamName, element, Locale.getDefault()); + baseDataValidator.reset().parameter(StandingInstructionApiConstants.instructionTypeParamName).value(standingInstructionType) + .notNull().inMinMaxRange(1, 2); + + final Integer recurrenceType = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.recurrenceTypeParamName, + element, Locale.getDefault()); + baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceTypeParamName).value(recurrenceType).notNull() + .inMinMaxRange(1, 2); + boolean isPeriodic = false; + if (recurrenceType != null) { + isPeriodic = AccountTransferRecurrenceType.fromInt(recurrenceType).isPeriodicRecurrence(); + } + + final Integer recurrenceFrequency = this.fromApiJsonHelper + .extractIntegerNamed(StandingInstructionApiConstants.recurrenceFrequencyParamName, element, Locale.getDefault()); + baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceFrequencyParamName).value(recurrenceFrequency) + .inMinMaxRange(0, 3); + + if (recurrenceFrequency != null) { + PeriodFrequencyType frequencyType = PeriodFrequencyType.fromInt(recurrenceFrequency); + if (frequencyType.isMonthly() || frequencyType.isYearly()) { + final MonthDay monthDay = this.fromApiJsonHelper + .extractMonthDayNamed(StandingInstructionApiConstants.recurrenceOnMonthDayParamName, element); + baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceOnMonthDayParamName).value(monthDay) + .notNull(); + } + } + + final Integer recurrenceInterval = this.fromApiJsonHelper + .extractIntegerNamed(StandingInstructionApiConstants.recurrenceIntervalParamName, element, Locale.getDefault()); + if (isPeriodic) { + baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceIntervalParamName).value(recurrenceInterval) + .notNull(); + baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceFrequencyParamName).value(recurrenceFrequency) + .notNull(); + } + baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceIntervalParamName).value(recurrenceInterval) + .integerGreaterThanZero(); + + final String name = this.fromApiJsonHelper.extractStringNamed(StandingInstructionApiConstants.nameParamName, element); + baseDataValidator.reset().parameter(StandingInstructionApiConstants.nameParamName).value(name).notNull(); + + final Integer toAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(toAccountTypeParamName, element); + if (toAccountType != null && (PortfolioAccountType.fromInt(toAccountType).isSavingsAccount() + || PortfolioAccountType.fromInt(toAccountType).isCurrentAccount())) { + baseDataValidator.reset().parameter(StandingInstructionApiConstants.instructionTypeParamName).value(standingInstructionType) + .notNull().inMinMaxRange(1, 1); + baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceTypeParamName).value(recurrenceType).notNull() + .inMinMaxRange(1, 1); + + } + if (standingInstructionType != null && StandingInstructionType.fromInt(standingInstructionType).isFixedAmoutTransfer()) { + baseDataValidator.reset().parameter(StandingInstructionApiConstants.amountParamName).value(transferAmount).notNull(); + } + + String errorCode = null; + AccountTransferType accountTransferType = AccountTransferType.fromInt(transferType); + final Integer fromAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(fromAccountTypeParamName, element); + if (fromAccountType != null && toAccountType != null) { + PortfolioAccountType fromPortfolioAccountType = PortfolioAccountType.fromInt(fromAccountType); + PortfolioAccountType toPortfolioAccountType = PortfolioAccountType.fromInt(toAccountType); + if (accountTransferType.isAccountTransfer() + && (fromPortfolioAccountType.isLoanAccount() || toPortfolioAccountType.isLoanAccount())) { + errorCode = "not.account.transfer"; + } else if (accountTransferType.isLoanRepayment() + && (fromPortfolioAccountType.isLoanAccount() + || (toPortfolioAccountType.isSavingsAccount() || toPortfolioAccountType.isCurrentAccount()))) { + errorCode = "not.loan.repayment"; + } + if (errorCode != null) { + baseDataValidator.reset().parameter(transferTypeParamName).failWithCode(errorCode); + } + } + + throwExceptionIfValidationWarningsExist(dataValidationErrors); + } + + public void validateForUpdate(final JsonCommand command) { + final String json = command.json(); + + if (StringUtils.isBlank(json)) { + throw new InvalidJsonException(); + } + + final Type typeOfMap = new TypeToken>() {}.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, UPDATE_REQUEST_DATA_PARAMETERS); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME); + + final JsonElement element = command.parsedJson(); + if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.validFromParamName, element)) { + final LocalDate validFrom = this.fromApiJsonHelper.extractLocalDateNamed(StandingInstructionApiConstants.validFromParamName, + element); + baseDataValidator.reset().parameter(StandingInstructionApiConstants.validFromParamName).value(validFrom).notNull(); + } + + if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.validTillParamName, element)) { + final LocalDate validTill = this.fromApiJsonHelper.extractLocalDateNamed(StandingInstructionApiConstants.validTillParamName, + element); + baseDataValidator.reset().parameter(StandingInstructionApiConstants.validTillParamName).value(validTill).notNull(); + } + + if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.amountParamName, element)) { + final BigDecimal transferAmount = this.fromApiJsonHelper + .extractBigDecimalWithLocaleNamed(StandingInstructionApiConstants.amountParamName, element); + baseDataValidator.reset().parameter(StandingInstructionApiConstants.amountParamName).value(transferAmount).positiveAmount(); + } + + if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.statusParamName, element)) { + final Integer status = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.statusParamName, element, + Locale.getDefault()); + baseDataValidator.reset().parameter(StandingInstructionApiConstants.statusParamName).value(status).notNull().inMinMaxRange(1, + 2); + } + + if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.priorityParamName, element)) { + final Integer priority = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.priorityParamName, element, + Locale.getDefault()); + baseDataValidator.reset().parameter(StandingInstructionApiConstants.priorityParamName).value(priority).notNull() + .inMinMaxRange(1, 4); + } + + if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.instructionTypeParamName, element)) { + final Integer standingInstructionType = this.fromApiJsonHelper + .extractIntegerNamed(StandingInstructionApiConstants.instructionTypeParamName, element, Locale.getDefault()); + baseDataValidator.reset().parameter(StandingInstructionApiConstants.instructionTypeParamName).value(standingInstructionType) + .notNull().inMinMaxRange(1, 2); + } + + if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.recurrenceTypeParamName, element)) { + final Integer recurrenceType = this.fromApiJsonHelper + .extractIntegerNamed(StandingInstructionApiConstants.recurrenceTypeParamName, element, Locale.getDefault()); + baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceTypeParamName).value(recurrenceType).notNull() + .inMinMaxRange(1, 2); + } + + if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.recurrenceFrequencyParamName, element)) { + final Integer recurrenceFrequency = this.fromApiJsonHelper + .extractIntegerNamed(StandingInstructionApiConstants.recurrenceFrequencyParamName, element, Locale.getDefault()); + baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceFrequencyParamName).value(recurrenceFrequency) + .inMinMaxRange(0, 3); + } + + if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.recurrenceIntervalParamName, element)) { + final Integer recurrenceInterval = this.fromApiJsonHelper + .extractIntegerNamed(StandingInstructionApiConstants.recurrenceIntervalParamName, element, Locale.getDefault()); + baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceIntervalParamName).value(recurrenceInterval) + .integerGreaterThanZero(); + } + + if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.nameParamName, element)) { + final String name = this.fromApiJsonHelper.extractStringNamed(StandingInstructionApiConstants.nameParamName, element); + baseDataValidator.reset().parameter(StandingInstructionApiConstants.nameParamName).value(name).notNull(); + } + + throwExceptionIfValidationWarningsExist(dataValidationErrors); + } + + private void throwExceptionIfValidationWarningsExist(final List dataValidationErrors) { + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/domain/AccountTransferDetailAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/domain/AccountTransferDetailAssembler.java index a8fe76f2a..985bc0b29 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/domain/AccountTransferDetailAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/domain/AccountTransferDetailAssembler.java @@ -1,214 +1,314 @@ -/** - * 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.account.domain; - -import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromAccountIdParamName; -import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromClientIdParamName; -import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromOfficeIdParamName; -import static org.apache.fineract.portfolio.account.AccountDetailConstants.toAccountIdParamName; -import static org.apache.fineract.portfolio.account.AccountDetailConstants.toClientIdParamName; -import static org.apache.fineract.portfolio.account.AccountDetailConstants.toOfficeIdParamName; -import static org.apache.fineract.portfolio.account.AccountDetailConstants.transferTypeParamName; - -import com.google.gson.JsonElement; -import java.util.Locale; -import org.apache.fineract.infrastructure.core.api.JsonCommand; -import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; -import org.apache.fineract.organisation.office.domain.Office; -import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper; -import org.apache.fineract.portfolio.client.domain.Client; -import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper; -import org.apache.fineract.portfolio.loanaccount.domain.Loan; -import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler; -import org.apache.fineract.portfolio.savings.domain.SavingsAccount; -import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -@Service -public class AccountTransferDetailAssembler { - - private final ClientRepositoryWrapper clientRepository; - private final OfficeRepositoryWrapper officeRepositoryWrapper; - private final SavingsAccountAssembler savingsAccountAssembler; - private final FromJsonHelper fromApiJsonHelper; - private final LoanAssembler loanAccountAssembler; - - @Autowired - public AccountTransferDetailAssembler(final ClientRepositoryWrapper clientRepository, - final OfficeRepositoryWrapper officeRepositoryWrapper, final SavingsAccountAssembler savingsAccountAssembler, - final FromJsonHelper fromApiJsonHelper, final LoanAssembler loanAccountAssembler) { - this.clientRepository = clientRepository; - this.officeRepositoryWrapper = officeRepositoryWrapper; - this.savingsAccountAssembler = savingsAccountAssembler; - this.fromApiJsonHelper = fromApiJsonHelper; - this.loanAccountAssembler = loanAccountAssembler; - } - - public AccountTransferDetails assembleSavingsToSavingsTransfer(final JsonCommand command) { - - final Long fromSavingsId = command.longValueOfParameterNamed(fromAccountIdParamName); - final SavingsAccount fromSavingsAccount = this.savingsAccountAssembler.assembleFrom(fromSavingsId, false); - - final boolean backdatedTxnsAllowedTill = false; - final Long toSavingsId = command.longValueOfParameterNamed(toAccountIdParamName); - final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsId, backdatedTxnsAllowedTill); - - return assembleSavingsToSavingsTransfer(command, fromSavingsAccount, toSavingsAccount); - - } - - public AccountTransferDetails assembleSavingsToLoanTransfer(final JsonCommand command) { - - final Long fromSavingsAccountId = command.longValueOfParameterNamed(fromAccountIdParamName); - final boolean backdatedTxnsAllowedTill = false; - final SavingsAccount fromSavingsAccount = this.savingsAccountAssembler.assembleFrom(fromSavingsAccountId, backdatedTxnsAllowedTill); - - final Long toLoanAccountId = command.longValueOfParameterNamed(toAccountIdParamName); - final Loan toLoanAccount = this.loanAccountAssembler.assembleFrom(toLoanAccountId); - - return assembleSavingsToLoanTransfer(command, fromSavingsAccount, toLoanAccount); - - } - - public AccountTransferDetails assembleLoanToSavingsTransfer(final JsonCommand command) { - - final Long fromLoanAccountId = command.longValueOfParameterNamed(fromAccountIdParamName); - final Loan fromLoanAccount = this.loanAccountAssembler.assembleFrom(fromLoanAccountId); - final boolean backdatedTxnsAllowedTill = false; - final Long toSavingsAccountId = command.longValueOfParameterNamed(toAccountIdParamName); - final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsAccountId, backdatedTxnsAllowedTill); - - return assembleLoanToSavingsTransfer(command, fromLoanAccount, toSavingsAccount); - } - - public AccountTransferDetails assembleSavingsToSavingsTransfer(final JsonCommand command, final SavingsAccount fromSavingsAccount, - final SavingsAccount toSavingsAccount) { - - final JsonElement element = command.parsedJson(); - - final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element); - final Office fromOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(fromOfficeId); - - final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element); - final Client fromClient = this.clientRepository.findOneWithNotFoundDetection(fromClientId); - - final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element); - final Office toOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(toOfficeId); - - final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element); - final Client toClient = this.clientRepository.findOneWithNotFoundDetection(toClientId); - - final Integer transfertype = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault()); - - return AccountTransferDetails.savingsToSavingsTransfer(fromOffice, fromClient, fromSavingsAccount, toOffice, toClient, - toSavingsAccount, transfertype); - - } - - public AccountTransferDetails assembleSavingsToLoanTransfer(final JsonCommand command, final SavingsAccount fromSavingsAccount, - final Loan toLoanAccount) { - - final JsonElement element = command.parsedJson(); - - final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element); - final Office fromOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(fromOfficeId); - - final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element); - final Client fromClient = this.clientRepository.findOneWithNotFoundDetection(fromClientId); - - final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element); - final Office toOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(toOfficeId); - - final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element); - final Client toClient = this.clientRepository.findOneWithNotFoundDetection(toClientId); - - final Integer transfertype = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault()); - - return AccountTransferDetails.savingsToLoanTransfer(fromOffice, fromClient, fromSavingsAccount, toOffice, toClient, toLoanAccount, - transfertype); - - } - - public AccountTransferDetails assembleLoanToSavingsTransfer(final JsonCommand command, final Loan fromLoanAccount, - final SavingsAccount toSavingsAccount) { - - final JsonElement element = command.parsedJson(); - - final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element); - final Office fromOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(fromOfficeId); - - final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element); - final Client fromClient = this.clientRepository.findOneWithNotFoundDetection(fromClientId); - - final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element); - final Office toOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(toOfficeId); - - final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element); - final Client toClient = this.clientRepository.findOneWithNotFoundDetection(toClientId); - final Integer transfertype = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault()); - - return AccountTransferDetails.loanTosavingsTransfer(fromOffice, fromClient, fromLoanAccount, toOffice, toClient, toSavingsAccount, - transfertype); - } - - public AccountTransferDetails assembleSavingsToLoanTransfer(final SavingsAccount fromSavingsAccount, final Loan toLoanAccount, - Integer transferType) { - final Office fromOffice = fromSavingsAccount.office(); - final Client fromClient = fromSavingsAccount.getClient(); - final Office toOffice = toLoanAccount.getOffice(); - final Client toClient = toLoanAccount.client(); - - return AccountTransferDetails.savingsToLoanTransfer(fromOffice, fromClient, fromSavingsAccount, toOffice, toClient, toLoanAccount, - transferType); - - } - - public AccountTransferDetails assembleSavingsToSavingsTransfer(final SavingsAccount fromSavingsAccount, - final SavingsAccount toSavingsAccount, Integer transferType) { - final Office fromOffice = fromSavingsAccount.office(); - final Client fromClient = fromSavingsAccount.getClient(); - final Office toOffice = toSavingsAccount.office(); - final Client toClient = toSavingsAccount.getClient(); - - return AccountTransferDetails.savingsToSavingsTransfer(fromOffice, fromClient, fromSavingsAccount, toOffice, toClient, - toSavingsAccount, transferType); - } - - public AccountTransferDetails assembleLoanToSavingsTransfer(final Loan fromLoanAccount, final SavingsAccount toSavingsAccount, - Integer transferType) { - final Office fromOffice = fromLoanAccount.getOffice(); - final Client fromClient = fromLoanAccount.client(); - final Office toOffice = toSavingsAccount.office(); - final Client toClient = toSavingsAccount.getClient(); - - return AccountTransferDetails.loanTosavingsTransfer(fromOffice, fromClient, fromLoanAccount, toOffice, toClient, toSavingsAccount, - transferType); - } - - public AccountTransferDetails assembleLoanToLoanTransfer(Loan fromLoanAccount, Loan toLoanAccount, Integer transferType) { - final Office fromOffice = fromLoanAccount.getOffice(); - final Client fromClient = fromLoanAccount.client(); - final Office toOffice = toLoanAccount.getOffice(); - final Client toClient = toLoanAccount.client(); - - return AccountTransferDetails.loanToLoanTransfer(fromOffice, fromClient, fromLoanAccount, toOffice, toClient, toLoanAccount, - transferType); - } -} +/** + * 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.account.domain; + +import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromAccountIdParamName; +import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromClientIdParamName; +import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromOfficeIdParamName; +import static org.apache.fineract.portfolio.account.AccountDetailConstants.toAccountIdParamName; +import static org.apache.fineract.portfolio.account.AccountDetailConstants.toClientIdParamName; +import static org.apache.fineract.portfolio.account.AccountDetailConstants.toOfficeIdParamName; +import static org.apache.fineract.portfolio.account.AccountDetailConstants.transferTypeParamName; + +import com.google.gson.JsonElement; +import java.util.Locale; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.organisation.office.domain.Office; +import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper; +import org.apache.fineract.portfolio.client.domain.Client; +import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper; +import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccount; +import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountRepository; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler; +import org.apache.fineract.portfolio.savings.domain.SavingsAccount; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class AccountTransferDetailAssembler { + + private final ClientRepositoryWrapper clientRepository; + private final OfficeRepositoryWrapper officeRepositoryWrapper; + private final SavingsAccountAssembler savingsAccountAssembler; + private final FromJsonHelper fromApiJsonHelper; + private final LoanAssembler loanAccountAssembler; + private final CurrentAccountRepository currentAccountRepository; + + @Autowired + public AccountTransferDetailAssembler(final ClientRepositoryWrapper clientRepository, + final OfficeRepositoryWrapper officeRepositoryWrapper, final SavingsAccountAssembler savingsAccountAssembler, + final FromJsonHelper fromApiJsonHelper, final LoanAssembler loanAccountAssembler, + final CurrentAccountRepository currentAccountRepository) { + this.clientRepository = clientRepository; + this.officeRepositoryWrapper = officeRepositoryWrapper; + this.savingsAccountAssembler = savingsAccountAssembler; + this.fromApiJsonHelper = fromApiJsonHelper; + this.loanAccountAssembler = loanAccountAssembler; + this.currentAccountRepository = currentAccountRepository; + } + + public AccountTransferDetails assembleSavingsToSavingsTransfer(final JsonCommand command) { + + final Long fromSavingsId = command.longValueOfParameterNamed(fromAccountIdParamName); + final SavingsAccount fromSavingsAccount = this.savingsAccountAssembler.assembleFrom(fromSavingsId, false); + + final boolean backdatedTxnsAllowedTill = false; + final Long toSavingsId = command.longValueOfParameterNamed(toAccountIdParamName); + final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsId, backdatedTxnsAllowedTill); + + return assembleSavingsToSavingsTransfer(command, fromSavingsAccount, toSavingsAccount); + + } + + public AccountTransferDetails assembleSavingsToLoanTransfer(final JsonCommand command) { + + final Long fromSavingsAccountId = command.longValueOfParameterNamed(fromAccountIdParamName); + final boolean backdatedTxnsAllowedTill = false; + final SavingsAccount fromSavingsAccount = this.savingsAccountAssembler.assembleFrom(fromSavingsAccountId, backdatedTxnsAllowedTill); + + final Long toLoanAccountId = command.longValueOfParameterNamed(toAccountIdParamName); + final Loan toLoanAccount = this.loanAccountAssembler.assembleFrom(toLoanAccountId); + + return assembleSavingsToLoanTransfer(command, fromSavingsAccount, toLoanAccount); + + } + + public AccountTransferDetails assembleLoanToSavingsTransfer(final JsonCommand command) { + + final Long fromLoanAccountId = command.longValueOfParameterNamed(fromAccountIdParamName); + final Loan fromLoanAccount = this.loanAccountAssembler.assembleFrom(fromLoanAccountId); + final boolean backdatedTxnsAllowedTill = false; + final Long toSavingsAccountId = command.longValueOfParameterNamed(toAccountIdParamName); + final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsAccountId, backdatedTxnsAllowedTill); + + return assembleLoanToSavingsTransfer(command, fromLoanAccount, toSavingsAccount); + } + + public AccountTransferDetails assembleSavingsToSavingsTransfer(final JsonCommand command, final SavingsAccount fromSavingsAccount, + final SavingsAccount toSavingsAccount) { + + final JsonElement element = command.parsedJson(); + + final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element); + final Office fromOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(fromOfficeId); + + final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element); + final Client fromClient = this.clientRepository.findOneWithNotFoundDetection(fromClientId); + + final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element); + final Office toOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(toOfficeId); + + final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element); + final Client toClient = this.clientRepository.findOneWithNotFoundDetection(toClientId); + + final Integer transfertype = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault()); + + return AccountTransferDetails.savingsToSavingsTransfer(fromOffice, fromClient, fromSavingsAccount, toOffice, toClient, + toSavingsAccount, transfertype); + + } + + public AccountTransferDetails assembleSavingsToLoanTransfer(final JsonCommand command, final SavingsAccount fromSavingsAccount, + final Loan toLoanAccount) { + + final JsonElement element = command.parsedJson(); + + final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element); + final Office fromOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(fromOfficeId); + + final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element); + final Client fromClient = this.clientRepository.findOneWithNotFoundDetection(fromClientId); + + final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element); + final Office toOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(toOfficeId); + + final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element); + final Client toClient = this.clientRepository.findOneWithNotFoundDetection(toClientId); + + final Integer transfertype = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault()); + + return AccountTransferDetails.savingsToLoanTransfer(fromOffice, fromClient, fromSavingsAccount, toOffice, toClient, toLoanAccount, + transfertype); + + } + + public AccountTransferDetails assembleLoanToSavingsTransfer(final JsonCommand command, final Loan fromLoanAccount, + final SavingsAccount toSavingsAccount) { + + final JsonElement element = command.parsedJson(); + + final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element); + final Office fromOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(fromOfficeId); + + final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element); + final Client fromClient = this.clientRepository.findOneWithNotFoundDetection(fromClientId); + + final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element); + final Office toOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(toOfficeId); + + final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element); + final Client toClient = this.clientRepository.findOneWithNotFoundDetection(toClientId); + final Integer transfertype = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault()); + + return AccountTransferDetails.loanTosavingsTransfer(fromOffice, fromClient, fromLoanAccount, toOffice, toClient, toSavingsAccount, + transfertype); + } + + public AccountTransferDetails assembleSavingsToLoanTransfer(final SavingsAccount fromSavingsAccount, final Loan toLoanAccount, + Integer transferType) { + final Office fromOffice = fromSavingsAccount.office(); + final Client fromClient = fromSavingsAccount.getClient(); + final Office toOffice = toLoanAccount.getOffice(); + final Client toClient = toLoanAccount.client(); + + return AccountTransferDetails.savingsToLoanTransfer(fromOffice, fromClient, fromSavingsAccount, toOffice, toClient, toLoanAccount, + transferType); + + } + + public AccountTransferDetails assembleSavingsToSavingsTransfer(final SavingsAccount fromSavingsAccount, + final SavingsAccount toSavingsAccount, Integer transferType) { + final Office fromOffice = fromSavingsAccount.office(); + final Client fromClient = fromSavingsAccount.getClient(); + final Office toOffice = toSavingsAccount.office(); + final Client toClient = toSavingsAccount.getClient(); + + return AccountTransferDetails.savingsToSavingsTransfer(fromOffice, fromClient, fromSavingsAccount, toOffice, toClient, + toSavingsAccount, transferType); + } + + public AccountTransferDetails assembleLoanToSavingsTransfer(final Loan fromLoanAccount, final SavingsAccount toSavingsAccount, + Integer transferType) { + final Office fromOffice = fromLoanAccount.getOffice(); + final Client fromClient = fromLoanAccount.client(); + final Office toOffice = toSavingsAccount.office(); + final Client toClient = toSavingsAccount.getClient(); + + return AccountTransferDetails.loanTosavingsTransfer(fromOffice, fromClient, fromLoanAccount, toOffice, toClient, toSavingsAccount, + transferType); + } + + public AccountTransferDetails assembleLoanToLoanTransfer(Loan fromLoanAccount, Loan toLoanAccount, Integer transferType) { + final Office fromOffice = fromLoanAccount.getOffice(); + final Client fromClient = fromLoanAccount.client(); + final Office toOffice = toLoanAccount.getOffice(); + final Client toClient = toLoanAccount.client(); + + return AccountTransferDetails.loanToLoanTransfer(fromOffice, fromClient, fromLoanAccount, toOffice, toClient, toLoanAccount, + transferType); + } + + // --- Current account assembly methods --- + + public AccountTransferDetails assembleCurrentToCurrentTransfer(final JsonCommand command) { + final Long fromCurrentAccountId = command.longValueOfParameterNamed(fromAccountIdParamName); + final CurrentAccount fromCurrentAccount = currentAccountRepository.findById(fromCurrentAccountId) + .orElseThrow(() -> new IllegalArgumentException("From current account not found: " + fromCurrentAccountId)); + + final Long toCurrentAccountId = command.longValueOfParameterNamed(toAccountIdParamName); + final CurrentAccount toCurrentAccount = currentAccountRepository.findById(toCurrentAccountId) + .orElseThrow(() -> new IllegalArgumentException("To current account not found: " + toCurrentAccountId)); + + final JsonElement element = command.parsedJson(); + final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element); + final Office fromOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(fromOfficeId); + final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element); + final Client fromClient = this.clientRepository.findOneWithNotFoundDetection(fromClientId); + final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element); + final Office toOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(toOfficeId); + final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element); + final Client toClient = this.clientRepository.findOneWithNotFoundDetection(toClientId); + final Integer transfertype = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault()); + + return AccountTransferDetails.currentToCurrentTransfer(fromOffice, fromClient, fromCurrentAccount, toOffice, toClient, + toCurrentAccount, transfertype); + } + + public AccountTransferDetails assembleCurrentToSavingsTransfer(final JsonCommand command) { + final Long fromCurrentAccountId = command.longValueOfParameterNamed(fromAccountIdParamName); + final CurrentAccount fromCurrentAccount = currentAccountRepository.findById(fromCurrentAccountId) + .orElseThrow(() -> new IllegalArgumentException("From current account not found: " + fromCurrentAccountId)); + + final Long toSavingsId = command.longValueOfParameterNamed(toAccountIdParamName); + final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsId, false); + + final JsonElement element = command.parsedJson(); + final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element); + final Office fromOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(fromOfficeId); + final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element); + final Client fromClient = this.clientRepository.findOneWithNotFoundDetection(fromClientId); + final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element); + final Office toOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(toOfficeId); + final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element); + final Client toClient = this.clientRepository.findOneWithNotFoundDetection(toClientId); + final Integer transfertype = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault()); + + return AccountTransferDetails.currentToSavingsTransfer(fromOffice, fromClient, fromCurrentAccount, toOffice, toClient, + toSavingsAccount, transfertype); + } + + public AccountTransferDetails assembleSavingsToCurrentTransfer(final JsonCommand command) { + final Long fromSavingsId = command.longValueOfParameterNamed(fromAccountIdParamName); + final SavingsAccount fromSavingsAccount = this.savingsAccountAssembler.assembleFrom(fromSavingsId, false); + + final Long toCurrentAccountId = command.longValueOfParameterNamed(toAccountIdParamName); + final CurrentAccount toCurrentAccount = currentAccountRepository.findById(toCurrentAccountId) + .orElseThrow(() -> new IllegalArgumentException("To current account not found: " + toCurrentAccountId)); + + final JsonElement element = command.parsedJson(); + final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element); + final Office fromOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(fromOfficeId); + final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element); + final Client fromClient = this.clientRepository.findOneWithNotFoundDetection(fromClientId); + final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element); + final Office toOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(toOfficeId); + final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element); + final Client toClient = this.clientRepository.findOneWithNotFoundDetection(toClientId); + final Integer transfertype = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault()); + + return AccountTransferDetails.savingsToCurrentTransfer(fromOffice, fromClient, fromSavingsAccount, toOffice, toClient, + toCurrentAccount, transfertype); + } + + public AccountTransferDetails assembleCurrentToLoanTransfer(final JsonCommand command) { + final Long fromCurrentAccountId = command.longValueOfParameterNamed(fromAccountIdParamName); + final CurrentAccount fromCurrentAccount = currentAccountRepository.findById(fromCurrentAccountId) + .orElseThrow(() -> new IllegalArgumentException("From current account not found: " + fromCurrentAccountId)); + + final Long toLoanAccountId = command.longValueOfParameterNamed(toAccountIdParamName); + final Loan toLoanAccount = this.loanAccountAssembler.assembleFrom(toLoanAccountId); + + final JsonElement element = command.parsedJson(); + final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element); + final Office fromOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(fromOfficeId); + final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element); + final Client fromClient = this.clientRepository.findOneWithNotFoundDetection(fromClientId); + final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element); + final Office toOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(toOfficeId); + final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element); + final Client toClient = this.clientRepository.findOneWithNotFoundDetection(toClientId); + final Integer transfertype = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault()); + + return AccountTransferDetails.currentToLoanTransfer(fromOffice, fromClient, fromCurrentAccount, toOffice, toClient, + toLoanAccount, transfertype); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/domain/AccountTransferDetails.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/domain/AccountTransferDetails.java index ff9b535a2..8a21f84c6 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/domain/AccountTransferDetails.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/domain/AccountTransferDetails.java @@ -1,165 +1,216 @@ -/** - * 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.account.domain; - -import jakarta.persistence.CascadeType; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.OneToMany; -import jakarta.persistence.OneToOne; -import jakarta.persistence.Table; -import java.util.ArrayList; -import java.util.List; -import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; -import org.apache.fineract.organisation.office.domain.Office; -import org.apache.fineract.portfolio.client.domain.Client; -import org.apache.fineract.portfolio.loanaccount.domain.Loan; -import org.apache.fineract.portfolio.savings.domain.SavingsAccount; - -@Entity -@Table(name = "m_account_transfer_details") -public class AccountTransferDetails extends AbstractPersistableCustom { - - @ManyToOne - @JoinColumn(name = "from_office_id", nullable = false) - private Office fromOffice; - - @ManyToOne - @JoinColumn(name = "from_client_id", nullable = false) - private Client fromClient; - - @ManyToOne - @JoinColumn(name = "from_savings_account_id", nullable = true) - private SavingsAccount fromSavingsAccount; - - @ManyToOne - @JoinColumn(name = "to_office_id", nullable = false) - private Office toOffice; - - @ManyToOne - @JoinColumn(name = "to_client_id", nullable = false) - private Client toClient; - - @ManyToOne - @JoinColumn(name = "to_savings_account_id", nullable = true) - private SavingsAccount toSavingsAccount; - - @ManyToOne - @JoinColumn(name = "to_loan_account_id", nullable = true) - private Loan toLoanAccount; - - @ManyToOne - @JoinColumn(name = "from_loan_account_id", nullable = true) - private Loan fromLoanAccount; - - @Column(name = "transfer_type") - private Integer transferType; - - @OneToMany(cascade = CascadeType.ALL, mappedBy = "accountTransferDetails", orphanRemoval = true, fetch = FetchType.EAGER) - private List accountTransferTransactions = new ArrayList<>(); - - @OneToOne(mappedBy = "accountTransferDetails", cascade = CascadeType.ALL, optional = true, orphanRemoval = true, fetch = FetchType.EAGER) - private AccountTransferStandingInstruction accountTransferStandingInstruction; - - public static AccountTransferDetails savingsToSavingsTransfer(final Office fromOffice, final Client fromClient, - final SavingsAccount fromSavingsAccount, final Office toOffice, final Client toClient, final SavingsAccount toSavingsAccount, - Integer transferType) { - - return new AccountTransferDetails(fromOffice, fromClient, fromSavingsAccount, null, toOffice, toClient, toSavingsAccount, null, - transferType, null); - } - - public static AccountTransferDetails savingsToLoanTransfer(final Office fromOffice, final Client fromClient, - final SavingsAccount fromSavingsAccount, final Office toOffice, final Client toClient, final Loan toLoanAccount, - Integer transferType) { - return new AccountTransferDetails(fromOffice, fromClient, fromSavingsAccount, null, toOffice, toClient, null, toLoanAccount, - transferType, null); - } - - public static AccountTransferDetails loanTosavingsTransfer(final Office fromOffice, final Client fromClient, final Loan fromLoanAccount, - final Office toOffice, final Client toClient, final SavingsAccount toSavingsAccount, Integer transferType) { - return new AccountTransferDetails(fromOffice, fromClient, null, fromLoanAccount, toOffice, toClient, toSavingsAccount, null, - transferType, null); - } - - protected AccountTransferDetails() { - // - } - - private AccountTransferDetails(final Office fromOffice, final Client fromClient, final SavingsAccount fromSavingsAccount, - final Loan fromLoanAccount, final Office toOffice, final Client toClient, final SavingsAccount toSavingsAccount, - final Loan toLoanAccount, final Integer transferType, - final AccountTransferStandingInstruction accountTransferStandingInstruction) { - this.fromOffice = fromOffice; - this.fromClient = fromClient; - this.fromSavingsAccount = fromSavingsAccount; - this.fromLoanAccount = fromLoanAccount; - this.toOffice = toOffice; - this.toClient = toClient; - this.toSavingsAccount = toSavingsAccount; - this.toLoanAccount = toLoanAccount; - this.transferType = transferType; - this.accountTransferStandingInstruction = accountTransferStandingInstruction; - } - - public SavingsAccount toSavingsAccount() { - return this.toSavingsAccount; - } - - public SavingsAccount fromSavingsAccount() { - return this.fromSavingsAccount; - } - - public void addAccountTransferTransaction(AccountTransferTransaction accountTransferTransaction) { - this.accountTransferTransactions.add(accountTransferTransaction); - } - - public void updateAccountTransferStandingInstruction(final AccountTransferStandingInstruction accountTransferStandingInstruction) { - this.accountTransferStandingInstruction = accountTransferStandingInstruction; - } - - public Loan toLoanAccount() { - return this.toLoanAccount; - } - - public Loan fromLoanAccount() { - return this.fromLoanAccount; - } - - public AccountTransferStandingInstruction accountTransferStandingInstruction() { - return this.accountTransferStandingInstruction; - } - - public AccountTransferType transferType() { - return AccountTransferType.fromInt(this.transferType); - } - - public static AccountTransferDetails loanToLoanTransfer(Office fromOffice, Client fromClient, Loan fromLoanAccount, Office toOffice, - Client toClient, Loan toLoanAccount, Integer transferType) { - return new AccountTransferDetails(fromOffice, fromClient, null, fromLoanAccount, toOffice, toClient, null, toLoanAccount, - transferType, null); - } - - public List getAccountTransferTransactions() { - return accountTransferTransactions; - } -} +/** + * 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.account.domain; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import java.util.ArrayList; +import java.util.List; +import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; +import org.apache.fineract.organisation.office.domain.Office; +import org.apache.fineract.portfolio.client.domain.Client; +import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccount; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.savings.domain.SavingsAccount; + +@Entity +@Table(name = "m_account_transfer_details") +public class AccountTransferDetails extends AbstractPersistableCustom { + + @ManyToOne + @JoinColumn(name = "from_office_id", nullable = false) + private Office fromOffice; + + @ManyToOne + @JoinColumn(name = "from_client_id", nullable = false) + private Client fromClient; + + @ManyToOne + @JoinColumn(name = "from_savings_account_id", nullable = true) + private SavingsAccount fromSavingsAccount; + + @ManyToOne + @JoinColumn(name = "to_office_id", nullable = false) + private Office toOffice; + + @ManyToOne + @JoinColumn(name = "to_client_id", nullable = false) + private Client toClient; + + @ManyToOne + @JoinColumn(name = "to_savings_account_id", nullable = true) + private SavingsAccount toSavingsAccount; + + @ManyToOne + @JoinColumn(name = "to_loan_account_id", nullable = true) + private Loan toLoanAccount; + + @ManyToOne + @JoinColumn(name = "from_loan_account_id", nullable = true) + private Loan fromLoanAccount; + + @ManyToOne + @JoinColumn(name = "from_current_account_id", nullable = true) + private CurrentAccount fromCurrentAccount; + + @ManyToOne + @JoinColumn(name = "to_current_account_id", nullable = true) + private CurrentAccount toCurrentAccount; + + @Column(name = "transfer_type") + private Integer transferType; + + @OneToMany(cascade = CascadeType.ALL, mappedBy = "accountTransferDetails", orphanRemoval = true, fetch = FetchType.EAGER) + private List accountTransferTransactions = new ArrayList<>(); + + @OneToOne(mappedBy = "accountTransferDetails", cascade = CascadeType.ALL, optional = true, orphanRemoval = true, fetch = FetchType.EAGER) + private AccountTransferStandingInstruction accountTransferStandingInstruction; + + public static AccountTransferDetails savingsToSavingsTransfer(final Office fromOffice, final Client fromClient, + final SavingsAccount fromSavingsAccount, final Office toOffice, final Client toClient, final SavingsAccount toSavingsAccount, + Integer transferType) { + + return new AccountTransferDetails(fromOffice, fromClient, fromSavingsAccount, null, null, toOffice, toClient, toSavingsAccount, + null, null, transferType, null); + } + + public static AccountTransferDetails savingsToLoanTransfer(final Office fromOffice, final Client fromClient, + final SavingsAccount fromSavingsAccount, final Office toOffice, final Client toClient, final Loan toLoanAccount, + Integer transferType) { + return new AccountTransferDetails(fromOffice, fromClient, fromSavingsAccount, null, null, toOffice, toClient, null, toLoanAccount, + null, transferType, null); + } + + public static AccountTransferDetails loanTosavingsTransfer(final Office fromOffice, final Client fromClient, final Loan fromLoanAccount, + final Office toOffice, final Client toClient, final SavingsAccount toSavingsAccount, Integer transferType) { + return new AccountTransferDetails(fromOffice, fromClient, null, fromLoanAccount, null, toOffice, toClient, toSavingsAccount, null, + null, transferType, null); + } + + protected AccountTransferDetails() { + // + } + + private AccountTransferDetails(final Office fromOffice, final Client fromClient, final SavingsAccount fromSavingsAccount, + final Loan fromLoanAccount, final CurrentAccount fromCurrentAccount, + final Office toOffice, final Client toClient, final SavingsAccount toSavingsAccount, + final Loan toLoanAccount, final CurrentAccount toCurrentAccount, + final Integer transferType, + final AccountTransferStandingInstruction accountTransferStandingInstruction) { + this.fromOffice = fromOffice; + this.fromClient = fromClient; + this.fromSavingsAccount = fromSavingsAccount; + this.fromLoanAccount = fromLoanAccount; + this.fromCurrentAccount = fromCurrentAccount; + this.toOffice = toOffice; + this.toClient = toClient; + this.toSavingsAccount = toSavingsAccount; + this.toLoanAccount = toLoanAccount; + this.toCurrentAccount = toCurrentAccount; + this.transferType = transferType; + this.accountTransferStandingInstruction = accountTransferStandingInstruction; + } + + public SavingsAccount toSavingsAccount() { + return this.toSavingsAccount; + } + + public SavingsAccount fromSavingsAccount() { + return this.fromSavingsAccount; + } + + public void addAccountTransferTransaction(AccountTransferTransaction accountTransferTransaction) { + this.accountTransferTransactions.add(accountTransferTransaction); + } + + public void updateAccountTransferStandingInstruction(final AccountTransferStandingInstruction accountTransferStandingInstruction) { + this.accountTransferStandingInstruction = accountTransferStandingInstruction; + } + + public Loan toLoanAccount() { + return this.toLoanAccount; + } + + public Loan fromLoanAccount() { + return this.fromLoanAccount; + } + + public CurrentAccount fromCurrentAccount() { + return this.fromCurrentAccount; + } + + public CurrentAccount toCurrentAccount() { + return this.toCurrentAccount; + } + + public AccountTransferStandingInstruction accountTransferStandingInstruction() { + return this.accountTransferStandingInstruction; + } + + public AccountTransferType transferType() { + return AccountTransferType.fromInt(this.transferType); + } + + public static AccountTransferDetails loanToLoanTransfer(Office fromOffice, Client fromClient, Loan fromLoanAccount, Office toOffice, + Client toClient, Loan toLoanAccount, Integer transferType) { + return new AccountTransferDetails(fromOffice, fromClient, null, fromLoanAccount, null, toOffice, toClient, null, toLoanAccount, + null, transferType, null); + } + + // --- Current account factory methods --- + + public static AccountTransferDetails currentToCurrentTransfer(final Office fromOffice, final Client fromClient, + final CurrentAccount fromCurrentAccount, final Office toOffice, final Client toClient, + final CurrentAccount toCurrentAccount, Integer transferType) { + return new AccountTransferDetails(fromOffice, fromClient, null, null, fromCurrentAccount, toOffice, toClient, null, null, + toCurrentAccount, transferType, null); + } + + public static AccountTransferDetails currentToSavingsTransfer(final Office fromOffice, final Client fromClient, + final CurrentAccount fromCurrentAccount, final Office toOffice, final Client toClient, + final SavingsAccount toSavingsAccount, Integer transferType) { + return new AccountTransferDetails(fromOffice, fromClient, null, null, fromCurrentAccount, toOffice, toClient, toSavingsAccount, + null, null, transferType, null); + } + + public static AccountTransferDetails savingsToCurrentTransfer(final Office fromOffice, final Client fromClient, + final SavingsAccount fromSavingsAccount, final Office toOffice, final Client toClient, + final CurrentAccount toCurrentAccount, Integer transferType) { + return new AccountTransferDetails(fromOffice, fromClient, fromSavingsAccount, null, null, toOffice, toClient, null, null, + toCurrentAccount, transferType, null); + } + + public static AccountTransferDetails currentToLoanTransfer(final Office fromOffice, final Client fromClient, + final CurrentAccount fromCurrentAccount, final Office toOffice, final Client toClient, + final Loan toLoanAccount, Integer transferType) { + return new AccountTransferDetails(fromOffice, fromClient, null, null, fromCurrentAccount, toOffice, toClient, null, toLoanAccount, + null, transferType, null); + } + + public List getAccountTransferTransactions() { + return accountTransferTransactions; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/domain/AccountTransferStandingInstruction.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/domain/AccountTransferStandingInstruction.java index fb4373087..780264b58 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/domain/AccountTransferStandingInstruction.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/domain/AccountTransferStandingInstruction.java @@ -1,290 +1,302 @@ -/** - * 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.account.domain; - -import static org.apache.fineract.portfolio.account.AccountDetailConstants.transferTypeParamName; -import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME; -import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.amountParamName; -import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.instructionTypeParamName; -import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.priorityParamName; -import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceFrequencyParamName; -import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceIntervalParamName; -import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceOnMonthDayParamName; -import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceTypeParamName; -import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.statusParamName; -import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.validFromParamName; -import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.validTillParamName; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.Table; -import jakarta.persistence.UniqueConstraint; -import java.math.BigDecimal; -import java.time.LocalDate; -import java.time.MonthDay; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -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.AbstractPersistableCustom; -import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; -import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; - -@Entity -@Table(name = "m_account_transfer_standing_instructions", uniqueConstraints = { - @UniqueConstraint(columnNames = { "name" }, name = "name") }) -public class AccountTransferStandingInstruction extends AbstractPersistableCustom { - - @ManyToOne - @JoinColumn(name = "account_transfer_details_id", nullable = true) - private AccountTransferDetails accountTransferDetails; - - @Column(name = "name") - private String name; - - @Column(name = "priority") - private Integer priority; - - @Column(name = "instruction_type") - private Integer instructionType; - - @Column(name = "status") - private Integer status; - - @Column(name = "amount", scale = 6, precision = 19, nullable = true) - private BigDecimal amount; - - @Column(name = "valid_from") - private LocalDate validFrom; - - @Column(name = "valid_till") - private LocalDate validTill; - - @Column(name = "recurrence_type") - private Integer recurrenceType; - - @Column(name = "recurrence_frequency") - private Integer recurrenceFrequency; - - @Column(name = "recurrence_interval") - private Integer recurrenceInterval; - - @Column(name = "recurrence_on_day") - private Integer recurrenceOnDay; - - @Column(name = "recurrence_on_month") - private Integer recurrenceOnMonth; - - @Column(name = "last_run_date") - private LocalDate latsRunDate; - - protected AccountTransferStandingInstruction() { - - } - - public static AccountTransferStandingInstruction create(final AccountTransferDetails accountTransferDetails, final String name, - final Integer priority, final Integer instructionType, final Integer status, final BigDecimal amount, final LocalDate validFrom, - final LocalDate validTill, final Integer recurrenceType, final Integer recurrenceFrequency, final Integer recurrenceInterval, - final MonthDay recurrenceOnMonthDay) { - Integer recurrenceOnDay = null; - Integer recurrenceOnMonth = null; - if (recurrenceOnMonthDay != null) { - recurrenceOnDay = recurrenceOnMonthDay.getDayOfMonth(); - recurrenceOnMonth = recurrenceOnMonthDay.getMonthValue(); - } - return new AccountTransferStandingInstruction(accountTransferDetails, name, priority, instructionType, status, amount, validFrom, - validTill, recurrenceType, recurrenceFrequency, recurrenceInterval, recurrenceOnDay, recurrenceOnMonth); - } - - private AccountTransferStandingInstruction(final AccountTransferDetails accountTransferDetails, final String name, - final Integer priority, final Integer instructionType, final Integer status, final BigDecimal amount, final LocalDate validFrom, - final LocalDate validTill, final Integer recurrenceType, final Integer recurrenceFrequency, final Integer recurrenceInterval, - final Integer recurrenceOnDay, final Integer recurrenceOnMonth) { - this.accountTransferDetails = accountTransferDetails; - this.name = name; - this.priority = priority; - this.instructionType = instructionType; - this.status = status; - this.amount = amount; - this.validFrom = validFrom; - this.validTill = validTill; - this.recurrenceType = recurrenceType; - this.recurrenceFrequency = recurrenceFrequency; - this.recurrenceInterval = recurrenceInterval; - this.recurrenceOnDay = recurrenceOnDay; - this.recurrenceOnMonth = recurrenceOnMonth; - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(STANDING_INSTRUCTION_RESOURCE_NAME); - - validateDependencies(baseDataValidator); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - public Map update(JsonCommand command) { - final Map actualChanges = new HashMap<>(); - - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(STANDING_INSTRUCTION_RESOURCE_NAME); - - if (StandingInstructionStatus.fromInt(this.status).isDeleted()) { - baseDataValidator.reset().parameter(statusParamName).failWithCode("can.not.modify.once.deleted"); - } - - if (command.isChangeInDateParameterNamed(validFromParamName, this.validFrom)) { - this.validFrom = command.localDateValueOfParameterNamed(validFromParamName); - actualChanges.put(validFromParamName, this.validFrom); - } - - if (command.isChangeInDateParameterNamed(validTillParamName, this.validTill)) { - this.validTill = command.localDateValueOfParameterNamed(validTillParamName); - actualChanges.put(validTillParamName, this.validTill); - } - - if (command.isChangeInBigDecimalParameterNamed(amountParamName, this.amount)) { - final BigDecimal newValue = command.bigDecimalValueOfParameterNamed(amountParamName); - actualChanges.put(amountParamName, newValue); - this.amount = newValue; - } - - if (command.isChangeInIntegerParameterNamed(statusParamName, this.status)) { - final Integer newValue = command.integerValueOfParameterNamed(statusParamName); - actualChanges.put(statusParamName, newValue); - this.status = newValue; - } - - if (command.isChangeInIntegerParameterNamed(priorityParamName, this.priority)) { - final Integer newValue = command.integerValueOfParameterNamed(priorityParamName); - actualChanges.put(priorityParamName, newValue); - this.priority = newValue; - } - - if (command.isChangeInIntegerParameterNamed(instructionTypeParamName, this.instructionType)) { - final Integer newValue = command.integerValueOfParameterNamed(instructionTypeParamName); - actualChanges.put(instructionTypeParamName, newValue); - this.instructionType = newValue; - } - - if (command.isChangeInIntegerParameterNamed(recurrenceTypeParamName, this.recurrenceType)) { - final Integer newValue = command.integerValueOfParameterNamed(recurrenceTypeParamName); - actualChanges.put(recurrenceTypeParamName, newValue); - this.recurrenceType = newValue; - } - - if (command.isChangeInIntegerParameterNamed(recurrenceFrequencyParamName, this.recurrenceFrequency)) { - final Integer newValue = command.integerValueOfParameterNamed(recurrenceFrequencyParamName); - actualChanges.put(recurrenceFrequencyParamName, newValue); - this.recurrenceFrequency = newValue; - } - - if (command.hasParameter(recurrenceOnMonthDayParamName)) { - final MonthDay monthDay = command.extractMonthDayNamed(recurrenceOnMonthDayParamName); - final String actualValueEntered = command.stringValueOfParameterNamed(recurrenceOnMonthDayParamName); - final Integer dayOfMonthValue = monthDay.getDayOfMonth(); - if (!this.recurrenceOnDay.equals(dayOfMonthValue)) { - actualChanges.put(recurrenceOnMonthDayParamName, actualValueEntered); - this.recurrenceOnDay = dayOfMonthValue; - } - - final Integer monthOfYear = monthDay.getMonthValue(); - if (!this.recurrenceOnMonth.equals(monthOfYear)) { - actualChanges.put(recurrenceOnMonthDayParamName, actualValueEntered); - this.recurrenceOnMonth = monthOfYear; - } - } - - if (command.isChangeInIntegerParameterNamed(recurrenceIntervalParamName, this.recurrenceInterval)) { - final Integer newValue = command.integerValueOfParameterNamed(recurrenceIntervalParamName); - actualChanges.put(recurrenceIntervalParamName, newValue); - this.recurrenceInterval = newValue; - } - validateDependencies(baseDataValidator); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - return actualChanges; - } - - private void validateDependencies(final DataValidatorBuilder baseDataValidator) { - - if (this.validTill != null && this.validFrom != null) { - baseDataValidator.reset().parameter(validTillParamName).value(this.validTill).validateDateAfter(this.validFrom); - } - - if (AccountTransferRecurrenceType.fromInt(recurrenceType).isPeriodicRecurrence()) { - baseDataValidator.reset().parameter(recurrenceFrequencyParamName).value(this.recurrenceFrequency).notNull(); - baseDataValidator.reset().parameter(recurrenceIntervalParamName).value(this.recurrenceInterval).notNull(); - if (this.recurrenceFrequency != null) { - PeriodFrequencyType frequencyType = PeriodFrequencyType.fromInt(this.recurrenceFrequency); - if (frequencyType.isMonthly()) { - baseDataValidator.reset().parameter(recurrenceOnMonthDayParamName).value(this.recurrenceOnDay).notNull(); - } else if (frequencyType.isYearly()) { - baseDataValidator.reset().parameter(recurrenceOnMonthDayParamName).value(this.recurrenceOnDay).notNull(); - baseDataValidator.reset().parameter(recurrenceOnMonthDayParamName).value(this.recurrenceOnMonth).notNull(); - } - } - } - - if (this.accountTransferDetails.toSavingsAccount() != null) { - baseDataValidator.reset().parameter(instructionTypeParamName).value(this.instructionType).notNull().inMinMaxRange(1, 1); - baseDataValidator.reset().parameter(recurrenceTypeParamName).value(this.recurrenceType).notNull().inMinMaxRange(1, 1); - } - - if (StandingInstructionType.fromInt(this.instructionType).isFixedAmoutTransfer()) { - baseDataValidator.reset().parameter(amountParamName).value(this.amount).notNull(); - } - - String errorCode = null; - if (this.accountTransferDetails.transferType().isAccountTransfer() - && (this.accountTransferDetails.fromSavingsAccount() == null || this.accountTransferDetails.toSavingsAccount() == null)) { - errorCode = "not.account.transfer"; - } else if (this.accountTransferDetails.transferType().isLoanRepayment() - && (this.accountTransferDetails.fromSavingsAccount() == null || this.accountTransferDetails.toLoanAccount() == null)) { - errorCode = "not.loan.repayment"; - } - if (errorCode != null) { - baseDataValidator.reset().parameter(transferTypeParamName).failWithCode(errorCode); - } - - } - - public void updateLatsRunDate(LocalDate latsRunDate) { - this.latsRunDate = latsRunDate; - } - - public void updateStatus(Integer status) { - this.status = status; - } - - /** - * delete the standing instruction by setting the status to 3 and appending "_deleted_" and the id to the name - **/ - public void delete() { - this.status = StandingInstructionStatus.DELETED.getValue(); - this.name = this.name + "_deleted_" + this.getId(); - } -} +/** + * 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.account.domain; + +import static org.apache.fineract.portfolio.account.AccountDetailConstants.transferTypeParamName; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.amountParamName; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.instructionTypeParamName; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.priorityParamName; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceFrequencyParamName; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceIntervalParamName; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceOnMonthDayParamName; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceTypeParamName; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.statusParamName; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.validFromParamName; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.validTillParamName; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.MonthDay; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +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.AbstractPersistableCustom; +import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; +import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; + +@Entity +@Table(name = "m_account_transfer_standing_instructions", uniqueConstraints = { + @UniqueConstraint(columnNames = { "name" }, name = "name") }) +public class AccountTransferStandingInstruction extends AbstractPersistableCustom { + + @ManyToOne + @JoinColumn(name = "account_transfer_details_id", nullable = true) + private AccountTransferDetails accountTransferDetails; + + @Column(name = "name") + private String name; + + @Column(name = "priority") + private Integer priority; + + @Column(name = "instruction_type") + private Integer instructionType; + + @Column(name = "status") + private Integer status; + + @Column(name = "amount", scale = 6, precision = 19, nullable = true) + private BigDecimal amount; + + @Column(name = "valid_from") + private LocalDate validFrom; + + @Column(name = "valid_till") + private LocalDate validTill; + + @Column(name = "recurrence_type") + private Integer recurrenceType; + + @Column(name = "recurrence_frequency") + private Integer recurrenceFrequency; + + @Column(name = "recurrence_interval") + private Integer recurrenceInterval; + + @Column(name = "recurrence_on_day") + private Integer recurrenceOnDay; + + @Column(name = "recurrence_on_month") + private Integer recurrenceOnMonth; + + @Column(name = "last_run_date") + private LocalDate latsRunDate; + + protected AccountTransferStandingInstruction() { + + } + + public static AccountTransferStandingInstruction create(final AccountTransferDetails accountTransferDetails, final String name, + final Integer priority, final Integer instructionType, final Integer status, final BigDecimal amount, final LocalDate validFrom, + final LocalDate validTill, final Integer recurrenceType, final Integer recurrenceFrequency, final Integer recurrenceInterval, + final MonthDay recurrenceOnMonthDay) { + Integer recurrenceOnDay = null; + Integer recurrenceOnMonth = null; + if (recurrenceOnMonthDay != null) { + recurrenceOnDay = recurrenceOnMonthDay.getDayOfMonth(); + recurrenceOnMonth = recurrenceOnMonthDay.getMonthValue(); + } + return new AccountTransferStandingInstruction(accountTransferDetails, name, priority, instructionType, status, amount, validFrom, + validTill, recurrenceType, recurrenceFrequency, recurrenceInterval, recurrenceOnDay, recurrenceOnMonth); + } + + private AccountTransferStandingInstruction(final AccountTransferDetails accountTransferDetails, final String name, + final Integer priority, final Integer instructionType, final Integer status, final BigDecimal amount, final LocalDate validFrom, + final LocalDate validTill, final Integer recurrenceType, final Integer recurrenceFrequency, final Integer recurrenceInterval, + final Integer recurrenceOnDay, final Integer recurrenceOnMonth) { + this.accountTransferDetails = accountTransferDetails; + this.name = name; + this.priority = priority; + this.instructionType = instructionType; + this.status = status; + this.amount = amount; + this.validFrom = validFrom; + this.validTill = validTill; + this.recurrenceType = recurrenceType; + this.recurrenceFrequency = recurrenceFrequency; + this.recurrenceInterval = recurrenceInterval; + this.recurrenceOnDay = recurrenceOnDay; + this.recurrenceOnMonth = recurrenceOnMonth; + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(STANDING_INSTRUCTION_RESOURCE_NAME); + + validateDependencies(baseDataValidator); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + public Map update(JsonCommand command) { + final Map actualChanges = new HashMap<>(); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(STANDING_INSTRUCTION_RESOURCE_NAME); + + if (StandingInstructionStatus.fromInt(this.status).isDeleted()) { + baseDataValidator.reset().parameter(statusParamName).failWithCode("can.not.modify.once.deleted"); + } + + if (command.isChangeInDateParameterNamed(validFromParamName, this.validFrom)) { + this.validFrom = command.localDateValueOfParameterNamed(validFromParamName); + actualChanges.put(validFromParamName, this.validFrom); + } + + if (command.isChangeInDateParameterNamed(validTillParamName, this.validTill)) { + this.validTill = command.localDateValueOfParameterNamed(validTillParamName); + actualChanges.put(validTillParamName, this.validTill); + } + + if (command.isChangeInBigDecimalParameterNamed(amountParamName, this.amount)) { + final BigDecimal newValue = command.bigDecimalValueOfParameterNamed(amountParamName); + actualChanges.put(amountParamName, newValue); + this.amount = newValue; + } + + if (command.isChangeInIntegerParameterNamed(statusParamName, this.status)) { + final Integer newValue = command.integerValueOfParameterNamed(statusParamName); + actualChanges.put(statusParamName, newValue); + this.status = newValue; + } + + if (command.isChangeInIntegerParameterNamed(priorityParamName, this.priority)) { + final Integer newValue = command.integerValueOfParameterNamed(priorityParamName); + actualChanges.put(priorityParamName, newValue); + this.priority = newValue; + } + + if (command.isChangeInIntegerParameterNamed(instructionTypeParamName, this.instructionType)) { + final Integer newValue = command.integerValueOfParameterNamed(instructionTypeParamName); + actualChanges.put(instructionTypeParamName, newValue); + this.instructionType = newValue; + } + + if (command.isChangeInIntegerParameterNamed(recurrenceTypeParamName, this.recurrenceType)) { + final Integer newValue = command.integerValueOfParameterNamed(recurrenceTypeParamName); + actualChanges.put(recurrenceTypeParamName, newValue); + this.recurrenceType = newValue; + } + + if (command.isChangeInIntegerParameterNamed(recurrenceFrequencyParamName, this.recurrenceFrequency)) { + final Integer newValue = command.integerValueOfParameterNamed(recurrenceFrequencyParamName); + actualChanges.put(recurrenceFrequencyParamName, newValue); + this.recurrenceFrequency = newValue; + } + + if (command.hasParameter(recurrenceOnMonthDayParamName)) { + final MonthDay monthDay = command.extractMonthDayNamed(recurrenceOnMonthDayParamName); + final String actualValueEntered = command.stringValueOfParameterNamed(recurrenceOnMonthDayParamName); + final Integer dayOfMonthValue = monthDay.getDayOfMonth(); + if (!this.recurrenceOnDay.equals(dayOfMonthValue)) { + actualChanges.put(recurrenceOnMonthDayParamName, actualValueEntered); + this.recurrenceOnDay = dayOfMonthValue; + } + + final Integer monthOfYear = monthDay.getMonthValue(); + if (!this.recurrenceOnMonth.equals(monthOfYear)) { + actualChanges.put(recurrenceOnMonthDayParamName, actualValueEntered); + this.recurrenceOnMonth = monthOfYear; + } + } + + if (command.isChangeInIntegerParameterNamed(recurrenceIntervalParamName, this.recurrenceInterval)) { + final Integer newValue = command.integerValueOfParameterNamed(recurrenceIntervalParamName); + actualChanges.put(recurrenceIntervalParamName, newValue); + this.recurrenceInterval = newValue; + } + validateDependencies(baseDataValidator); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + return actualChanges; + } + + private void validateDependencies(final DataValidatorBuilder baseDataValidator) { + + if (this.validTill != null && this.validFrom != null) { + baseDataValidator.reset().parameter(validTillParamName).value(this.validTill).validateDateAfter(this.validFrom); + } + + if (AccountTransferRecurrenceType.fromInt(recurrenceType).isPeriodicRecurrence()) { + baseDataValidator.reset().parameter(recurrenceFrequencyParamName).value(this.recurrenceFrequency).notNull(); + baseDataValidator.reset().parameter(recurrenceIntervalParamName).value(this.recurrenceInterval).notNull(); + if (this.recurrenceFrequency != null) { + PeriodFrequencyType frequencyType = PeriodFrequencyType.fromInt(this.recurrenceFrequency); + if (frequencyType.isMonthly()) { + baseDataValidator.reset().parameter(recurrenceOnMonthDayParamName).value(this.recurrenceOnDay).notNull(); + } else if (frequencyType.isYearly()) { + baseDataValidator.reset().parameter(recurrenceOnMonthDayParamName).value(this.recurrenceOnDay).notNull(); + baseDataValidator.reset().parameter(recurrenceOnMonthDayParamName).value(this.recurrenceOnMonth).notNull(); + } + } + } + + // For savings-to-savings or current-to-savings: enforce Fixed instruction + Periodic recurrence + if (this.accountTransferDetails.toSavingsAccount() != null + || this.accountTransferDetails.toCurrentAccount() != null) { + baseDataValidator.reset().parameter(instructionTypeParamName).value(this.instructionType).notNull().inMinMaxRange(1, 1); + baseDataValidator.reset().parameter(recurrenceTypeParamName).value(this.recurrenceType).notNull().inMinMaxRange(1, 1); + } + + if (StandingInstructionType.fromInt(this.instructionType).isFixedAmoutTransfer()) { + baseDataValidator.reset().parameter(amountParamName).value(this.amount).notNull(); + } + + // Account Transfer: from must be savings OR current; to must be savings OR current + boolean fromAccountValid = this.accountTransferDetails.fromSavingsAccount() != null + || this.accountTransferDetails.fromCurrentAccount() != null; + boolean toAccountValid = this.accountTransferDetails.toSavingsAccount() != null + || this.accountTransferDetails.toCurrentAccount() != null; + + // Loan Repayment: from must be savings OR current; to must be loan + boolean fromAccountValidForLoan = this.accountTransferDetails.fromSavingsAccount() != null + || this.accountTransferDetails.fromCurrentAccount() != null; + + String errorCode = null; + if (this.accountTransferDetails.transferType().isAccountTransfer() + && !(fromAccountValid && toAccountValid)) { + errorCode = "not.account.transfer"; + } else if (this.accountTransferDetails.transferType().isLoanRepayment() + && !(fromAccountValidForLoan && this.accountTransferDetails.toLoanAccount() != null)) { + errorCode = "not.loan.repayment"; + } + if (errorCode != null) { + baseDataValidator.reset().parameter(transferTypeParamName).failWithCode(errorCode); + } + + } + + public void updateLatsRunDate(LocalDate latsRunDate) { + this.latsRunDate = latsRunDate; + } + + public void updateStatus(Integer status) { + this.status = status; + } + + /** + * delete the standing instruction by setting the status to 3 and appending "_deleted_" and the id to the name + **/ + public void delete() { + this.status = StandingInstructionStatus.DELETED.getValue(); + this.name = this.name + "_deleted_" + this.getId(); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/domain/StandingInstructionAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/domain/StandingInstructionAssembler.java index 079bda738..f84ca4ee2 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/domain/StandingInstructionAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/domain/StandingInstructionAssembler.java @@ -1,93 +1,143 @@ -/** - * 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.account.domain; - -import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.amountParamName; -import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.instructionTypeParamName; -import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.nameParamName; -import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.priorityParamName; -import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceFrequencyParamName; -import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceIntervalParamName; -import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceOnMonthDayParamName; -import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceTypeParamName; -import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.statusParamName; -import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.validFromParamName; -import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.validTillParamName; - -import java.math.BigDecimal; -import java.time.LocalDate; -import java.time.MonthDay; -import org.apache.fineract.infrastructure.core.api.JsonCommand; -import org.apache.fineract.organisation.monetary.domain.Money; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -@Service -public class StandingInstructionAssembler { - - private final AccountTransferDetailAssembler accountTransferDetailAssembler; - - @Autowired - public StandingInstructionAssembler(final AccountTransferDetailAssembler accountTransferDetailAssembler) { - - this.accountTransferDetailAssembler = accountTransferDetailAssembler; - } - - public AccountTransferDetails assembleSavingsToSavingsTransfer(final JsonCommand command) { - final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleSavingsToSavingsTransfer(command); - assembleStandingInstruction(command, accountTransferDetails); - return accountTransferDetails; - } - - public void assembleStandingInstruction(final JsonCommand command, final AccountTransferDetails accountTransferDetails) { - final LocalDate validFrom = command.localDateValueOfParameterNamed(validFromParamName); - final LocalDate validTill = command.localDateValueOfParameterNamed(validTillParamName); - BigDecimal amount = null; - final BigDecimal transferAmount = command.bigDecimalValueOfParameterNamed(amountParamName); - if (transferAmount != null) { - final Money monetaryAmount = Money.of(accountTransferDetails.fromSavingsAccount().getCurrency(), transferAmount); - amount = monetaryAmount.getAmount(); - } - final Integer status = command.integerValueOfParameterNamed(statusParamName); - final Integer priority = command.integerValueOfParameterNamed(priorityParamName); - final Integer standingInstructionType = command.integerValueOfParameterNamed(instructionTypeParamName); - final Integer recurrenceType = command.integerValueOfParameterNamed(recurrenceTypeParamName); - final Integer recurrenceFrequency = command.integerValueOfParameterNamed(recurrenceFrequencyParamName); - final MonthDay recurrenceOnMonthDay = command.extractMonthDayNamed(recurrenceOnMonthDayParamName); - final Integer recurrenceInterval = command.integerValueOfParameterNamed(recurrenceIntervalParamName); - final String name = command.stringValueOfParameterNamed(nameParamName); - AccountTransferStandingInstruction accountTransferStandingInstruction = AccountTransferStandingInstruction.create( - accountTransferDetails, name, priority, standingInstructionType, status, amount, validFrom, validTill, recurrenceType, - recurrenceFrequency, recurrenceInterval, recurrenceOnMonthDay); - accountTransferDetails.updateAccountTransferStandingInstruction(accountTransferStandingInstruction); - } - - public AccountTransferDetails assembleSavingsToLoanTransfer(final JsonCommand command) { - final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleSavingsToLoanTransfer(command); - assembleStandingInstruction(command, accountTransferDetails); - return accountTransferDetails; - } - - public AccountTransferDetails assembleLoanToSavingsTransfer(final JsonCommand command) { - final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleLoanToSavingsTransfer(command); - assembleStandingInstruction(command, accountTransferDetails); - return accountTransferDetails; - } - -} +/** + * 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.account.domain; + +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.amountParamName; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.instructionTypeParamName; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.nameParamName; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.priorityParamName; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceFrequencyParamName; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceIntervalParamName; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceOnMonthDayParamName; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceTypeParamName; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.statusParamName; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.validFromParamName; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.validTillParamName; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.MonthDay; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccount; +import org.apache.fineract.portfolio.savings.domain.SavingsAccount; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class StandingInstructionAssembler { + + private final AccountTransferDetailAssembler accountTransferDetailAssembler; + + @Autowired + public StandingInstructionAssembler(final AccountTransferDetailAssembler accountTransferDetailAssembler) { + + this.accountTransferDetailAssembler = accountTransferDetailAssembler; + } + + public AccountTransferDetails assembleSavingsToSavingsTransfer(final JsonCommand command) { + final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleSavingsToSavingsTransfer(command); + assembleStandingInstruction(command, accountTransferDetails); + return accountTransferDetails; + } + + public void assembleStandingInstruction(final JsonCommand command, final AccountTransferDetails accountTransferDetails) { + final LocalDate validFrom = command.localDateValueOfParameterNamed(validFromParamName); + final LocalDate validTill = command.localDateValueOfParameterNamed(validTillParamName); + BigDecimal amount = null; + final BigDecimal transferAmount = command.bigDecimalValueOfParameterNamed(amountParamName); + if (transferAmount != null) { + final MonetaryCurrency currency = resolveCurrency(accountTransferDetails); + if (currency != null) { + final Money monetaryAmount = Money.of(currency, transferAmount); + amount = monetaryAmount.getAmount(); + } else { + amount = transferAmount; + } + } + final Integer status = command.integerValueOfParameterNamed(statusParamName); + final Integer priority = command.integerValueOfParameterNamed(priorityParamName); + final Integer standingInstructionType = command.integerValueOfParameterNamed(instructionTypeParamName); + final Integer recurrenceType = command.integerValueOfParameterNamed(recurrenceTypeParamName); + final Integer recurrenceFrequency = command.integerValueOfParameterNamed(recurrenceFrequencyParamName); + final MonthDay recurrenceOnMonthDay = command.extractMonthDayNamed(recurrenceOnMonthDayParamName); + final Integer recurrenceInterval = command.integerValueOfParameterNamed(recurrenceIntervalParamName); + final String name = command.stringValueOfParameterNamed(nameParamName); + AccountTransferStandingInstruction accountTransferStandingInstruction = AccountTransferStandingInstruction.create( + accountTransferDetails, name, priority, standingInstructionType, status, amount, validFrom, validTill, recurrenceType, + recurrenceFrequency, recurrenceInterval, recurrenceOnMonthDay); + accountTransferDetails.updateAccountTransferStandingInstruction(accountTransferStandingInstruction); + } + + /** + * Resolves the currency from the "from" account — savings, loan (null), or current account. + */ + private MonetaryCurrency resolveCurrency(final AccountTransferDetails details) { + final SavingsAccount fromSavings = details.fromSavingsAccount(); + if (fromSavings != null) { + return fromSavings.getCurrency(); + } + final CurrentAccount fromCurrent = details.fromCurrentAccount(); + if (fromCurrent != null) { + return new MonetaryCurrency(fromCurrent.getCurrencyCode(), fromCurrent.getCurrencyDigits(), + fromCurrent.getCurrencyMultiplesOf()); + } + return null; // loan accounts don't have a direct currency on AccountTransferDetails + } + + public AccountTransferDetails assembleSavingsToLoanTransfer(final JsonCommand command) { + final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleSavingsToLoanTransfer(command); + assembleStandingInstruction(command, accountTransferDetails); + return accountTransferDetails; + } + + public AccountTransferDetails assembleLoanToSavingsTransfer(final JsonCommand command) { + final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleLoanToSavingsTransfer(command); + assembleStandingInstruction(command, accountTransferDetails); + return accountTransferDetails; + } + + // --- Current account methods --- + + public AccountTransferDetails assembleCurrentToCurrentTransfer(final JsonCommand command) { + final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleCurrentToCurrentTransfer(command); + assembleStandingInstruction(command, accountTransferDetails); + return accountTransferDetails; + } + + public AccountTransferDetails assembleCurrentToSavingsTransfer(final JsonCommand command) { + final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleCurrentToSavingsTransfer(command); + assembleStandingInstruction(command, accountTransferDetails); + return accountTransferDetails; + } + + public AccountTransferDetails assembleSavingsToCurrentTransfer(final JsonCommand command) { + final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleSavingsToCurrentTransfer(command); + assembleStandingInstruction(command, accountTransferDetails); + return accountTransferDetails; + } + + public AccountTransferDetails assembleCurrentToLoanTransfer(final JsonCommand command) { + final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleCurrentToLoanTransfer(command); + assembleStandingInstruction(command, accountTransferDetails); + return accountTransferDetails; + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/jobs/executestandinginstructions/ExecuteStandingInstructionsTasklet.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/jobs/executestandinginstructions/ExecuteStandingInstructionsTasklet.java index 0b4a9e064..352c012e3 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/jobs/executestandinginstructions/ExecuteStandingInstructionsTasklet.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/jobs/executestandinginstructions/ExecuteStandingInstructionsTasklet.java @@ -1,172 +1,172 @@ -/** - * 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.account.jobs.executestandinginstructions; - -import java.math.BigDecimal; -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.fineract.infrastructure.core.domain.ExternalId; -import org.apache.fineract.infrastructure.core.exception.AbstractPlatformServiceUnavailableException; -import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; -import org.apache.fineract.infrastructure.core.service.DateUtils; -import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; -import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException; -import org.apache.fineract.portfolio.account.data.AccountTransferDTO; -import org.apache.fineract.portfolio.account.data.StandingInstructionData; -import org.apache.fineract.portfolio.account.data.StandingInstructionDuesData; -import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType; -import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus; -import org.apache.fineract.portfolio.account.domain.StandingInstructionType; -import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService; -import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformService; -import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; -import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.DefaultScheduledDateGenerator; -import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.ScheduledDateGenerator; -import org.apache.fineract.portfolio.savings.domain.SavingsAccount; -import org.apache.fineract.portfolio.savings.exception.InsufficientAccountBalanceException; -import org.springframework.batch.core.StepContribution; -import org.springframework.batch.core.scope.context.ChunkContext; -import org.springframework.batch.core.step.tasklet.Tasklet; -import org.springframework.batch.repeat.RepeatStatus; -import org.springframework.jdbc.core.JdbcTemplate; - -@Slf4j -@RequiredArgsConstructor -public class ExecuteStandingInstructionsTasklet implements Tasklet { - - private final StandingInstructionReadPlatformService standingInstructionReadPlatformService; - private final JdbcTemplate jdbcTemplate; - private final DatabaseSpecificSQLGenerator sqlGenerator; - private final AccountTransfersWritePlatformService accountTransfersWritePlatformService; - - @Override - public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { - Collection instructionData = standingInstructionReadPlatformService - .retrieveAll(StandingInstructionStatus.ACTIVE.getValue()); - List errors = new ArrayList<>(); - for (StandingInstructionData data : instructionData) { - boolean isDueForTransfer = false; - AccountTransferRecurrenceType recurrenceType = data.getRecurrenceType(); - StandingInstructionType instructionType = data.getInstructionType(); - LocalDate transactionDate = DateUtils.getBusinessLocalDate(); - if (recurrenceType.isPeriodicRecurrence()) { - final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator(); - PeriodFrequencyType frequencyType = data.getRecurrenceFrequency(); - LocalDate startDate = data.getValidFrom(); - if (frequencyType.isMonthly()) { - startDate = startDate.withDayOfMonth(data.getRecurrenceOnDay()); - if (DateUtils.isBefore(startDate, data.getValidFrom())) { - startDate = startDate.plusMonths(1); - } - } else if (frequencyType.isYearly()) { - startDate = startDate.withDayOfMonth(data.getRecurrenceOnDay()).withMonth(data.getRecurrenceOnMonth()); - if (DateUtils.isBefore(startDate, data.getValidFrom())) { - startDate = startDate.plusYears(1); - } - } - isDueForTransfer = scheduledDateGenerator.isDateFallsInSchedule(frequencyType, data.getRecurrenceInterval(), startDate, - transactionDate); - - } - BigDecimal transactionAmount = data.getAmount(); - if (data.getToAccountType().isLoanAccount() - && (recurrenceType.isDuesRecurrence() || (isDueForTransfer && instructionType.isDuesAmoutTransfer()))) { - StandingInstructionDuesData standingInstructionDuesData = standingInstructionReadPlatformService - .retriveLoanDuesData(data.getToAccount().getId()); - if (data.getInstructionType().isDuesAmoutTransfer()) { - transactionAmount = standingInstructionDuesData.totalDueAmount(); - } - if (recurrenceType.isDuesRecurrence()) { - isDueForTransfer = isDueForTransfer(standingInstructionDuesData); - } - } - - if (isDueForTransfer && transactionAmount != null && transactionAmount.compareTo(BigDecimal.ZERO) > 0) { - final SavingsAccount fromSavingsAccount = null; - final boolean isRegularTransaction = true; - final boolean isExceptionForBalanceCheck = false; - AccountTransferDTO accountTransferDTO = new AccountTransferDTO(transactionDate, transactionAmount, - data.getFromAccountType(), data.getToAccountType(), data.getFromAccount().getId(), data.getToAccount().getId(), - data.getName() + " Standing instruction trasfer ", null, null, null, null, data.toTransferType(), null, null, - data.getTransferType().getValue(), null, null, ExternalId.empty(), null, null, fromSavingsAccount, - isRegularTransaction, isExceptionForBalanceCheck); - final boolean transferCompleted = transferAmount(errors, accountTransferDTO, data.getId()); - - if (transferCompleted) { - final String updateQuery = "UPDATE m_account_transfer_standing_instructions SET last_run_date = ? where id = ?"; - jdbcTemplate.update(updateQuery, transactionDate, data.getId()); - } - - } - } - if (!errors.isEmpty()) { - throw new JobExecutionException(errors); - } - return RepeatStatus.FINISHED; - } - - private boolean transferAmount(final List errors, final AccountTransferDTO accountTransferDTO, final Long instructionId) { - boolean transferCompleted = true; - StringBuilder errorLog = new StringBuilder(); - StringBuilder updateQuery = new StringBuilder( - "INSERT INTO m_account_transfer_standing_instructions_history (standing_instruction_id, " + sqlGenerator.escape("status") - + ", amount,execution_time, error_log) VALUES ("); - try { - accountTransfersWritePlatformService.transferFunds(accountTransferDTO); - } catch (final PlatformApiDataValidationException e) { - errors.add(new Exception("Validation exception while transfering funds for standing Instruction id" + instructionId + " from " - + accountTransferDTO.getFromAccountId() + " to " + accountTransferDTO.getToAccountId(), e)); - errorLog.append("Validation exception while trasfering funds ").append(e.getDefaultUserMessage()); - } catch (final InsufficientAccountBalanceException e) { - errors.add(new Exception("InsufficientAccountBalance Exception while trasfering funds for standing Instruction id" - + instructionId + " from " + accountTransferDTO.getFromAccountId() + " to " + accountTransferDTO.getToAccountId(), e)); - errorLog.append("InsufficientAccountBalance Exception "); - } catch (final AbstractPlatformServiceUnavailableException e) { - errors.add(new Exception("Platform exception while trasfering funds for standing Instruction id" + instructionId + " from " - + accountTransferDTO.getFromAccountId() + " to " + accountTransferDTO.getToAccountId(), e)); - errorLog.append("Platform exception while trasfering funds ").append(e.getDefaultUserMessage()); - } catch (Exception e) { - errors.add(new Exception("Unhandled System Exception while trasfering funds for standing Instruction id" + instructionId - + " from " + accountTransferDTO.getFromAccountId() + " to " + accountTransferDTO.getToAccountId(), e)); - errorLog.append("Exception while trasfering funds ").append(e.getMessage()); - - } - updateQuery.append(instructionId).append(","); - if (errorLog.length() > 0) { - transferCompleted = false; - updateQuery.append("'failed'").append(","); - } else { - updateQuery.append("'success'").append(","); - } - updateQuery.append(accountTransferDTO.getTransactionAmount().doubleValue()); - updateQuery.append(", now(),"); - updateQuery.append("'").append(errorLog).append("')"); - jdbcTemplate.update(updateQuery.toString()); - return transferCompleted; - } - - public boolean isDueForTransfer(StandingInstructionDuesData standingInstructionDuesData) { - return standingInstructionDuesData.dueDate() != null - && !standingInstructionDuesData.dueDate().isAfter(LocalDate.now(DateUtils.getDateTimeZoneOfTenant())); - } -} +/** + * 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.account.jobs.executestandinginstructions; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.infrastructure.core.domain.ExternalId; +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformServiceUnavailableException; +import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; +import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException; +import org.apache.fineract.portfolio.account.data.AccountTransferDTO; +import org.apache.fineract.portfolio.account.data.StandingInstructionData; +import org.apache.fineract.portfolio.account.data.StandingInstructionDuesData; +import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType; +import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus; +import org.apache.fineract.portfolio.account.domain.StandingInstructionType; +import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService; +import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformService; +import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; +import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.DefaultScheduledDateGenerator; +import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.ScheduledDateGenerator; +import org.apache.fineract.portfolio.savings.domain.SavingsAccount; +import org.apache.fineract.portfolio.savings.exception.InsufficientAccountBalanceException; +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.jdbc.core.JdbcTemplate; + +@Slf4j +@RequiredArgsConstructor +public class ExecuteStandingInstructionsTasklet implements Tasklet { + + private final StandingInstructionReadPlatformService standingInstructionReadPlatformService; + private final JdbcTemplate jdbcTemplate; + private final DatabaseSpecificSQLGenerator sqlGenerator; + private final AccountTransfersWritePlatformService accountTransfersWritePlatformService; + + @Override + public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { + Collection instructionData = standingInstructionReadPlatformService + .retrieveAll(StandingInstructionStatus.ACTIVE.getValue()); + List errors = new ArrayList<>(); + for (StandingInstructionData data : instructionData) { + boolean isDueForTransfer = false; + AccountTransferRecurrenceType recurrenceType = data.recurrenceTypeEnum(); + StandingInstructionType instructionType = data.instructionTypeEnum(); + LocalDate transactionDate = DateUtils.getBusinessLocalDate(); + if (recurrenceType.isPeriodicRecurrence()) { + final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator(); + PeriodFrequencyType frequencyType = data.recurrenceFrequencyEnum(); + LocalDate startDate = data.getValidFrom(); + if (frequencyType.isMonthly()) { + startDate = startDate.withDayOfMonth(data.getRecurrenceOnDay()); + if (DateUtils.isBefore(startDate, data.getValidFrom())) { + startDate = startDate.plusMonths(1); + } + } else if (frequencyType.isYearly()) { + startDate = startDate.withDayOfMonth(data.getRecurrenceOnDay()).withMonth(data.getRecurrenceOnMonth()); + if (DateUtils.isBefore(startDate, data.getValidFrom())) { + startDate = startDate.plusYears(1); + } + } + isDueForTransfer = scheduledDateGenerator.isDateFallsInSchedule(frequencyType, data.getRecurrenceInterval(), startDate, + transactionDate); + + } + BigDecimal transactionAmount = data.getAmount(); + if (data.toAccountTypeEnum() != null && data.toAccountTypeEnum().isLoanAccount() + && (recurrenceType.isDuesRecurrence() || (isDueForTransfer && instructionType.isDuesAmoutTransfer()))) { + StandingInstructionDuesData standingInstructionDuesData = standingInstructionReadPlatformService + .retriveLoanDuesData(data.getToAccount().getId()); + if (data.instructionTypeEnum().isDuesAmoutTransfer()) { + transactionAmount = standingInstructionDuesData.totalDueAmount(); + } + if (recurrenceType.isDuesRecurrence()) { + isDueForTransfer = isDueForTransfer(standingInstructionDuesData); + } + } + + if (isDueForTransfer && transactionAmount != null && transactionAmount.compareTo(BigDecimal.ZERO) > 0) { + final SavingsAccount fromSavingsAccount = null; + final boolean isRegularTransaction = true; + final boolean isExceptionForBalanceCheck = false; + AccountTransferDTO accountTransferDTO = new AccountTransferDTO(transactionDate, transactionAmount, + data.fromAccountTypeEnum(), data.toAccountTypeEnum(), data.getFromAccount().getId(), data.getToAccount().getId(), + data.getName() + " Standing instruction trasfer ", null, null, null, null, data.toTransferType(), null, null, + data.transferTypeEnum().getValue(), null, null, ExternalId.empty(), null, null, fromSavingsAccount, + isRegularTransaction, isExceptionForBalanceCheck); + final boolean transferCompleted = transferAmount(errors, accountTransferDTO, data.getId()); + + if (transferCompleted) { + final String updateQuery = "UPDATE m_account_transfer_standing_instructions SET last_run_date = ? where id = ?"; + jdbcTemplate.update(updateQuery, transactionDate, data.getId()); + } + + } + } + if (!errors.isEmpty()) { + throw new JobExecutionException(errors); + } + return RepeatStatus.FINISHED; + } + + private boolean transferAmount(final List errors, final AccountTransferDTO accountTransferDTO, final Long instructionId) { + boolean transferCompleted = true; + StringBuilder errorLog = new StringBuilder(); + StringBuilder updateQuery = new StringBuilder( + "INSERT INTO m_account_transfer_standing_instructions_history (standing_instruction_id, " + sqlGenerator.escape("status") + + ", amount,execution_time, error_log) VALUES ("); + try { + accountTransfersWritePlatformService.transferFunds(accountTransferDTO); + } catch (final PlatformApiDataValidationException e) { + errors.add(new Exception("Validation exception while transfering funds for standing Instruction id" + instructionId + " from " + + accountTransferDTO.getFromAccountId() + " to " + accountTransferDTO.getToAccountId(), e)); + errorLog.append("Validation exception while trasfering funds ").append(e.getDefaultUserMessage()); + } catch (final InsufficientAccountBalanceException e) { + errors.add(new Exception("InsufficientAccountBalance Exception while trasfering funds for standing Instruction id" + + instructionId + " from " + accountTransferDTO.getFromAccountId() + " to " + accountTransferDTO.getToAccountId(), e)); + errorLog.append("InsufficientAccountBalance Exception "); + } catch (final AbstractPlatformServiceUnavailableException e) { + errors.add(new Exception("Platform exception while trasfering funds for standing Instruction id" + instructionId + " from " + + accountTransferDTO.getFromAccountId() + " to " + accountTransferDTO.getToAccountId(), e)); + errorLog.append("Platform exception while trasfering funds ").append(e.getDefaultUserMessage()); + } catch (Exception e) { + errors.add(new Exception("Unhandled System Exception while trasfering funds for standing Instruction id" + instructionId + + " from " + accountTransferDTO.getFromAccountId() + " to " + accountTransferDTO.getToAccountId(), e)); + errorLog.append("Exception while trasfering funds ").append(e.getMessage()); + + } + updateQuery.append(instructionId).append(","); + if (errorLog.length() > 0) { + transferCompleted = false; + updateQuery.append("'failed'").append(","); + } else { + updateQuery.append("'success'").append(","); + } + updateQuery.append(accountTransferDTO.getTransactionAmount().doubleValue()); + updateQuery.append(", now(),"); + updateQuery.append("'").append(errorLog).append("')"); + jdbcTemplate.update(updateQuery.toString()); + return transferCompleted; + } + + public boolean isDueForTransfer(StandingInstructionDuesData standingInstructionDuesData) { + return standingInstructionDuesData.dueDate() != null + && !standingInstructionDuesData.dueDate().isAfter(LocalDate.now(DateUtils.getDateTimeZoneOfTenant())); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransferEnumerations.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransferEnumerations.java index 991857970..3a6c5c358 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransferEnumerations.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransferEnumerations.java @@ -1,202 +1,206 @@ -/** - * 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.account.service; - -import org.apache.fineract.infrastructure.core.data.EnumOptionData; -import org.apache.fineract.portfolio.account.PortfolioAccountType; -import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType; -import org.apache.fineract.portfolio.account.domain.AccountTransferType; -import org.apache.fineract.portfolio.account.domain.StandingInstructionPriority; -import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus; -import org.apache.fineract.portfolio.account.domain.StandingInstructionType; - -public final class AccountTransferEnumerations { - - private AccountTransferEnumerations() { - - } - - public static EnumOptionData accountType(final Integer type) { - return accountType(PortfolioAccountType.fromInt(type)); - } - - public static EnumOptionData accountType(final PortfolioAccountType type) { - - EnumOptionData optionData = null; - - if (type != null) { - switch (type) { - case INVALID: - break; - case LOAN: - optionData = new EnumOptionData(PortfolioAccountType.LOAN.getValue().longValue(), PortfolioAccountType.LOAN.getCode(), - "Loan Account"); - break; - case SAVINGS: - optionData = new EnumOptionData(PortfolioAccountType.SAVINGS.getValue().longValue(), - PortfolioAccountType.SAVINGS.getCode(), "Savings Account"); - break; - } - } - - return optionData; - } - - public static EnumOptionData recurrenceType(final Integer type) { - return recurrenceType(AccountTransferRecurrenceType.fromInt(type)); - } - - public static EnumOptionData recurrenceType(final AccountTransferRecurrenceType type) { - EnumOptionData optionData = null; - if (type != null) { - switch (type) { - case INVALID: - break; - case PERIODIC: - optionData = new EnumOptionData(AccountTransferRecurrenceType.PERIODIC.getValue().longValue(), - AccountTransferRecurrenceType.PERIODIC.getCode(), "Periodic Recurrence"); - break; - case AS_PER_DUES: - optionData = new EnumOptionData(AccountTransferRecurrenceType.AS_PER_DUES.getValue().longValue(), - AccountTransferRecurrenceType.AS_PER_DUES.getCode(), "As Per Dues Recurrence"); - break; - } - } - return optionData; - } - - public static EnumOptionData transferType(final Integer type) { - return transferType(AccountTransferType.fromInt(type)); - } - - public static EnumOptionData transferType(final AccountTransferType type) { - EnumOptionData optionData = null; - if (type != null) { - switch (type) { - case INVALID: - break; - case ACCOUNT_TRANSFER: - optionData = new EnumOptionData(AccountTransferType.ACCOUNT_TRANSFER.getValue().longValue(), - AccountTransferType.ACCOUNT_TRANSFER.getCode(), "Account Transfer"); - break; - case LOAN_REPAYMENT: - optionData = new EnumOptionData(AccountTransferType.LOAN_REPAYMENT.getValue().longValue(), - AccountTransferType.LOAN_REPAYMENT.getCode(), "Loan Repayment"); - break; - case CHARGE_PAYMENT: - optionData = new EnumOptionData(AccountTransferType.CHARGE_PAYMENT.getValue().longValue(), - AccountTransferType.CHARGE_PAYMENT.getCode(), "Charge Payment"); - break; - case INTEREST_TRANSFER: - optionData = new EnumOptionData(AccountTransferType.INTEREST_TRANSFER.getValue().longValue(), - AccountTransferType.INTEREST_TRANSFER.getCode(), "Interest Transfer"); - break; - case LOAN_DOWN_PAYMENT: - optionData = new EnumOptionData(AccountTransferType.LOAN_DOWN_PAYMENT.getValue().longValue(), - AccountTransferType.LOAN_DOWN_PAYMENT.getCode(), "Loan Down Payment"); - break; - - } - } - return optionData; - } - - public static EnumOptionData standingInstructionPriority(final Integer type) { - return standingInstructionPriority(StandingInstructionPriority.fromInt(type)); - } - - public static EnumOptionData standingInstructionPriority(final StandingInstructionPriority type) { - EnumOptionData optionData = null; - if (type != null) { - switch (type) { - case INVALID: - break; - case HIGH: - optionData = new EnumOptionData(StandingInstructionPriority.HIGH.getValue().longValue(), - StandingInstructionPriority.HIGH.getCode(), "High Priority"); - break; - case LOW: - optionData = new EnumOptionData(StandingInstructionPriority.LOW.getValue().longValue(), - StandingInstructionPriority.LOW.getCode(), "Low Priority"); - break; - case MEDIUM: - optionData = new EnumOptionData(StandingInstructionPriority.MEDIUM.getValue().longValue(), - StandingInstructionPriority.MEDIUM.getCode(), "Medium Priority"); - break; - case URGENT: - optionData = new EnumOptionData(StandingInstructionPriority.URGENT.getValue().longValue(), - StandingInstructionPriority.URGENT.getCode(), "Urgent Priority"); - break; - } - } - return optionData; - } - - public static EnumOptionData standingInstructionStatus(final Integer type) { - return standingInstructionStatus(StandingInstructionStatus.fromInt(type)); - } - - public static EnumOptionData standingInstructionStatus(final StandingInstructionStatus type) { - EnumOptionData optionData = null; - if (type != null) { - switch (type) { - case INVALID: - break; - case ACTIVE: - optionData = new EnumOptionData(StandingInstructionStatus.ACTIVE.getValue().longValue(), - StandingInstructionStatus.ACTIVE.getCode(), "Active"); - break; - case DELETED: - optionData = new EnumOptionData(StandingInstructionStatus.DELETED.getValue().longValue(), - StandingInstructionStatus.DELETED.getCode(), "Deleted"); - break; - case DISABLED: - optionData = new EnumOptionData(StandingInstructionStatus.DISABLED.getValue().longValue(), - StandingInstructionStatus.DISABLED.getCode(), "Disabled"); - break; - } - } - return optionData; - } - - public static EnumOptionData standingInstructionType(final Integer type) { - return standingInstructionType(StandingInstructionType.fromInt(type)); - } - - public static EnumOptionData standingInstructionType(final StandingInstructionType type) { - EnumOptionData optionData = null; - if (type != null) { - switch (type) { - case INVALID: - break; - case DUES: - optionData = new EnumOptionData(StandingInstructionType.DUES.getValue().longValue(), - StandingInstructionType.DUES.getCode(), "Dues"); - break; - case FIXED: - optionData = new EnumOptionData(StandingInstructionType.FIXED.getValue().longValue(), - StandingInstructionType.FIXED.getCode(), "Fixed"); - break; - - } - } - return optionData; - } - -} +/** + * 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.account.service; + +import org.apache.fineract.infrastructure.core.data.EnumOptionData; +import org.apache.fineract.portfolio.account.PortfolioAccountType; +import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType; +import org.apache.fineract.portfolio.account.domain.AccountTransferType; +import org.apache.fineract.portfolio.account.domain.StandingInstructionPriority; +import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus; +import org.apache.fineract.portfolio.account.domain.StandingInstructionType; + +public final class AccountTransferEnumerations { + + private AccountTransferEnumerations() { + + } + + public static EnumOptionData accountType(final Integer type) { + return accountType(PortfolioAccountType.fromInt(type)); + } + + public static EnumOptionData accountType(final PortfolioAccountType type) { + + EnumOptionData optionData = null; + + if (type != null) { + switch (type) { + case INVALID: + break; + case LOAN: + optionData = new EnumOptionData(PortfolioAccountType.LOAN.getValue().longValue(), PortfolioAccountType.LOAN.getCode(), + "Loan Account"); + break; + case SAVINGS: + optionData = new EnumOptionData(PortfolioAccountType.SAVINGS.getValue().longValue(), + PortfolioAccountType.SAVINGS.getCode(), "Savings Account"); + break; + case CURRENT: + optionData = new EnumOptionData(PortfolioAccountType.CURRENT.getValue().longValue(), + PortfolioAccountType.CURRENT.getCode(), "Current Account"); + break; + } + } + + return optionData; + } + + public static EnumOptionData recurrenceType(final Integer type) { + return recurrenceType(AccountTransferRecurrenceType.fromInt(type)); + } + + public static EnumOptionData recurrenceType(final AccountTransferRecurrenceType type) { + EnumOptionData optionData = null; + if (type != null) { + switch (type) { + case INVALID: + break; + case PERIODIC: + optionData = new EnumOptionData(AccountTransferRecurrenceType.PERIODIC.getValue().longValue(), + AccountTransferRecurrenceType.PERIODIC.getCode(), "Periodic Recurrence"); + break; + case AS_PER_DUES: + optionData = new EnumOptionData(AccountTransferRecurrenceType.AS_PER_DUES.getValue().longValue(), + AccountTransferRecurrenceType.AS_PER_DUES.getCode(), "As Per Dues Recurrence"); + break; + } + } + return optionData; + } + + public static EnumOptionData transferType(final Integer type) { + return transferType(AccountTransferType.fromInt(type)); + } + + public static EnumOptionData transferType(final AccountTransferType type) { + EnumOptionData optionData = null; + if (type != null) { + switch (type) { + case INVALID: + break; + case ACCOUNT_TRANSFER: + optionData = new EnumOptionData(AccountTransferType.ACCOUNT_TRANSFER.getValue().longValue(), + AccountTransferType.ACCOUNT_TRANSFER.getCode(), "Account Transfer"); + break; + case LOAN_REPAYMENT: + optionData = new EnumOptionData(AccountTransferType.LOAN_REPAYMENT.getValue().longValue(), + AccountTransferType.LOAN_REPAYMENT.getCode(), "Loan Repayment"); + break; + case CHARGE_PAYMENT: + optionData = new EnumOptionData(AccountTransferType.CHARGE_PAYMENT.getValue().longValue(), + AccountTransferType.CHARGE_PAYMENT.getCode(), "Charge Payment"); + break; + case INTEREST_TRANSFER: + optionData = new EnumOptionData(AccountTransferType.INTEREST_TRANSFER.getValue().longValue(), + AccountTransferType.INTEREST_TRANSFER.getCode(), "Interest Transfer"); + break; + case LOAN_DOWN_PAYMENT: + optionData = new EnumOptionData(AccountTransferType.LOAN_DOWN_PAYMENT.getValue().longValue(), + AccountTransferType.LOAN_DOWN_PAYMENT.getCode(), "Loan Down Payment"); + break; + + } + } + return optionData; + } + + public static EnumOptionData standingInstructionPriority(final Integer type) { + return standingInstructionPriority(StandingInstructionPriority.fromInt(type)); + } + + public static EnumOptionData standingInstructionPriority(final StandingInstructionPriority type) { + EnumOptionData optionData = null; + if (type != null) { + switch (type) { + case INVALID: + break; + case HIGH: + optionData = new EnumOptionData(StandingInstructionPriority.HIGH.getValue().longValue(), + StandingInstructionPriority.HIGH.getCode(), "High Priority"); + break; + case LOW: + optionData = new EnumOptionData(StandingInstructionPriority.LOW.getValue().longValue(), + StandingInstructionPriority.LOW.getCode(), "Low Priority"); + break; + case MEDIUM: + optionData = new EnumOptionData(StandingInstructionPriority.MEDIUM.getValue().longValue(), + StandingInstructionPriority.MEDIUM.getCode(), "Medium Priority"); + break; + case URGENT: + optionData = new EnumOptionData(StandingInstructionPriority.URGENT.getValue().longValue(), + StandingInstructionPriority.URGENT.getCode(), "Urgent Priority"); + break; + } + } + return optionData; + } + + public static EnumOptionData standingInstructionStatus(final Integer type) { + return standingInstructionStatus(StandingInstructionStatus.fromInt(type)); + } + + public static EnumOptionData standingInstructionStatus(final StandingInstructionStatus type) { + EnumOptionData optionData = null; + if (type != null) { + switch (type) { + case INVALID: + break; + case ACTIVE: + optionData = new EnumOptionData(StandingInstructionStatus.ACTIVE.getValue().longValue(), + StandingInstructionStatus.ACTIVE.getCode(), "Active"); + break; + case DELETED: + optionData = new EnumOptionData(StandingInstructionStatus.DELETED.getValue().longValue(), + StandingInstructionStatus.DELETED.getCode(), "Deleted"); + break; + case DISABLED: + optionData = new EnumOptionData(StandingInstructionStatus.DISABLED.getValue().longValue(), + StandingInstructionStatus.DISABLED.getCode(), "Disabled"); + break; + } + } + return optionData; + } + + public static EnumOptionData standingInstructionType(final Integer type) { + return standingInstructionType(StandingInstructionType.fromInt(type)); + } + + public static EnumOptionData standingInstructionType(final StandingInstructionType type) { + EnumOptionData optionData = null; + if (type != null) { + switch (type) { + case INVALID: + break; + case DUES: + optionData = new EnumOptionData(StandingInstructionType.DUES.getValue().longValue(), + StandingInstructionType.DUES.getCode(), "Dues"); + break; + case FIXED: + optionData = new EnumOptionData(StandingInstructionType.FIXED.getValue().longValue(), + StandingInstructionType.FIXED.getCode(), "Fixed"); + break; + + } + } + return optionData; + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransfersWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransfersWritePlatformServiceImpl.java index 7eea9969e..aaf24bd3d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransfersWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransfersWritePlatformServiceImpl.java @@ -1,561 +1,753 @@ -/** - * 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.account.service; - -import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromAccountIdParamName; -import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromAccountTypeParamName; -import static org.apache.fineract.portfolio.account.AccountDetailConstants.toAccountIdParamName; -import static org.apache.fineract.portfolio.account.AccountDetailConstants.toAccountTypeParamName; -import static org.apache.fineract.portfolio.account.api.AccountTransfersApiConstants.transferAmountParamName; -import static org.apache.fineract.portfolio.account.api.AccountTransfersApiConstants.transferDateParamName; - -import com.google.common.collect.Lists; -import java.math.BigDecimal; -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Locale; -import lombok.RequiredArgsConstructor; -import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; -import org.apache.fineract.infrastructure.core.api.JsonCommand; -import org.apache.fineract.infrastructure.core.config.FineractProperties; -import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; -import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; -import org.apache.fineract.infrastructure.core.domain.ExternalId; -import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException; -import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; -import org.apache.fineract.portfolio.account.PortfolioAccountType; -import org.apache.fineract.portfolio.account.data.AccountTransferDTO; -import org.apache.fineract.portfolio.account.data.AccountTransfersDataValidator; -import org.apache.fineract.portfolio.account.domain.AccountTransferAssembler; -import org.apache.fineract.portfolio.account.domain.AccountTransferDetailRepository; -import org.apache.fineract.portfolio.account.domain.AccountTransferDetails; -import org.apache.fineract.portfolio.account.domain.AccountTransferRepository; -import org.apache.fineract.portfolio.account.domain.AccountTransferTransaction; -import org.apache.fineract.portfolio.account.domain.AccountTransferType; -import org.apache.fineract.portfolio.account.exception.DifferentCurrenciesException; -import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO; -import org.apache.fineract.portfolio.loanaccount.domain.Loan; -import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService; -import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; -import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; -import org.apache.fineract.portfolio.loanaccount.exception.InvalidPaidInAdvanceAmountException; -import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler; -import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService; -import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail; -import org.apache.fineract.portfolio.savings.SavingsTransactionBooleanValues; -import org.apache.fineract.portfolio.savings.domain.GSIMRepositoy; -import org.apache.fineract.portfolio.savings.domain.GroupSavingsIndividualMonitoring; -import org.apache.fineract.portfolio.savings.domain.SavingsAccount; -import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler; -import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction; -import org.apache.fineract.portfolio.savings.service.SavingsAccountDomainService; -import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService; -import org.springframework.transaction.annotation.Transactional; - -@RequiredArgsConstructor -public class AccountTransfersWritePlatformServiceImpl implements AccountTransfersWritePlatformService { - - private final AccountTransfersDataValidator accountTransfersDataValidator; - private final AccountTransferAssembler accountTransferAssembler; - private final AccountTransferRepository accountTransferRepository; - private final SavingsAccountAssembler savingsAccountAssembler; - private final SavingsAccountDomainService savingsAccountDomainService; - private final LoanAssembler loanAccountAssembler; - private final LoanAccountDomainService loanAccountDomainService; - private final SavingsAccountWritePlatformService savingsAccountWritePlatformService; - private final AccountTransferDetailRepository accountTransferDetailRepository; - private final LoanReadPlatformService loanReadPlatformService; - private final GSIMRepositoy gsimRepository; - private final ConfigurationDomainService configurationDomainService; - private final ExternalIdFactory externalIdFactory; - private final FineractProperties fineractProperties; - - @Transactional - @Override - public CommandProcessingResult create(final JsonCommand command) { - boolean isRegularTransaction = true; - - this.accountTransfersDataValidator.validate(command); - - final LocalDate transactionDate = command.localDateValueOfParameterNamed(transferDateParamName); - final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed(transferAmountParamName); - - final Locale locale = command.extractLocale(); - final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); - - final Integer fromAccountTypeId = command.integerValueSansLocaleOfParameterNamed(fromAccountTypeParamName); - final PortfolioAccountType fromAccountType = PortfolioAccountType.fromInt(fromAccountTypeId); - - final Integer toAccountTypeId = command.integerValueSansLocaleOfParameterNamed(toAccountTypeParamName); - final PortfolioAccountType toAccountType = PortfolioAccountType.fromInt(toAccountTypeId); - - final PaymentDetail paymentDetail = null; - Long fromSavingsAccountId = null; - Long transferDetailId = null; - boolean isInterestTransfer = false; - boolean isAccountTransfer = true; - Long fromLoanAccountId = null; - boolean isWithdrawBalance = false; - final boolean backdatedTxnsAllowedTill = false; - - if (isSavingsToSavingsAccountTransfer(fromAccountType, toAccountType)) { - - fromSavingsAccountId = command.longValueOfParameterNamed(fromAccountIdParamName); - final SavingsAccount fromSavingsAccount = this.savingsAccountAssembler.assembleFrom(fromSavingsAccountId, - backdatedTxnsAllowedTill); - - final SavingsTransactionBooleanValues transactionBooleanValues = new SavingsTransactionBooleanValues(isAccountTransfer, - isRegularTransaction, fromSavingsAccount.isWithdrawalFeeApplicableForTransfer(), isInterestTransfer, isWithdrawBalance); - final SavingsAccountTransaction withdrawal = this.savingsAccountDomainService.handleWithdrawal(fromSavingsAccount, fmt, - transactionDate, transactionAmount, paymentDetail, transactionBooleanValues, backdatedTxnsAllowedTill); - - final Long toSavingsId = command.longValueOfParameterNamed(toAccountIdParamName); - final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsId, backdatedTxnsAllowedTill); - - final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount, fmt, transactionDate, - transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill); - - if (!fromSavingsAccount.getCurrency().getCode().equals(toSavingsAccount.getCurrency().getCode())) { - throw new DifferentCurrenciesException(fromSavingsAccount.getCurrency().getCode(), - toSavingsAccount.getCurrency().getCode()); - } - - final AccountTransferDetails accountTransferDetails = this.accountTransferAssembler.assembleSavingsToSavingsTransfer(command, - fromSavingsAccount, toSavingsAccount, withdrawal, deposit); - this.accountTransferDetailRepository.saveAndFlush(accountTransferDetails); - transferDetailId = accountTransferDetails.getId(); - - } else if (isSavingsToLoanAccountTransfer(fromAccountType, toAccountType)) { - // - fromSavingsAccountId = command.longValueOfParameterNamed(fromAccountIdParamName); - final SavingsAccount fromSavingsAccount = this.savingsAccountAssembler.assembleFrom(fromSavingsAccountId, - backdatedTxnsAllowedTill); - - final SavingsTransactionBooleanValues transactionBooleanValues = new SavingsTransactionBooleanValues(isAccountTransfer, - isRegularTransaction, fromSavingsAccount.isWithdrawalFeeApplicableForTransfer(), isInterestTransfer, isWithdrawBalance); - final SavingsAccountTransaction withdrawal = this.savingsAccountDomainService.handleWithdrawal(fromSavingsAccount, fmt, - transactionDate, transactionAmount, paymentDetail, transactionBooleanValues, backdatedTxnsAllowedTill); - - final Long toLoanAccountId = command.longValueOfParameterNamed(toAccountIdParamName); - Loan toLoanAccount = this.loanAccountAssembler.assembleFrom(toLoanAccountId); - - final Boolean isHolidayValidationDone = false; - final HolidayDetailDTO holidayDetailDto = null; - final boolean isRecoveryRepayment = false; - final String chargeRefundChargeType = null; - - ExternalId externalId = externalIdFactory.create(); - final LoanTransaction loanRepaymentTransaction = this.loanAccountDomainService.makeRepayment(LoanTransactionType.REPAYMENT, - toLoanAccount, transactionDate, transactionAmount, paymentDetail, null, externalId, isRecoveryRepayment, - chargeRefundChargeType, isAccountTransfer, holidayDetailDto, isHolidayValidationDone); - toLoanAccount = loanRepaymentTransaction.getLoan(); - final AccountTransferDetails accountTransferDetails = this.accountTransferAssembler.assembleSavingsToLoanTransfer(command, - fromSavingsAccount, toLoanAccount, withdrawal, loanRepaymentTransaction); - this.accountTransferDetailRepository.saveAndFlush(accountTransferDetails); - transferDetailId = accountTransferDetails.getId(); - - } else if (isLoanToSavingsAccountTransfer(fromAccountType, toAccountType)) { - // FIXME - kw - ADD overpaid loan to savings account transfer - // support. - - fromLoanAccountId = command.longValueOfParameterNamed(fromAccountIdParamName); - final Loan fromLoanAccount = this.loanAccountAssembler.assembleFrom(fromLoanAccountId); - ExternalId externalId = externalIdFactory.create(); - final LoanTransaction loanRefundTransaction = this.loanAccountDomainService.makeRefund(fromLoanAccountId, - new CommandProcessingResultBuilder(), transactionDate, transactionAmount, paymentDetail, null, externalId); - - final Long toSavingsAccountId = command.longValueOfParameterNamed(toAccountIdParamName); - final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsAccountId, backdatedTxnsAllowedTill); - - final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount, fmt, transactionDate, - transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill); - - final AccountTransferDetails accountTransferDetails = this.accountTransferAssembler.assembleLoanToSavingsTransfer(command, - fromLoanAccount, toSavingsAccount, deposit, loanRefundTransaction); - this.accountTransferDetailRepository.saveAndFlush(accountTransferDetails); - transferDetailId = accountTransferDetails.getId(); - - } - - final CommandProcessingResultBuilder builder = new CommandProcessingResultBuilder().withEntityId(transferDetailId); - - if (fromAccountType.isSavingsAccount()) { - builder.withSavingsId(fromSavingsAccountId); - } - if (fromAccountType.isLoanAccount()) { - builder.withLoanId(fromLoanAccountId); - } - - return builder.build(); - } - - @Override - @Transactional - public void reverseTransfersWithFromAccountType(final Long accountNumber, final PortfolioAccountType accountTypeId) { - List accountTransfers = null; - if (accountTypeId.isLoanAccount()) { - accountTransfers = this.accountTransferRepository.findByFromLoanId(accountNumber); - } - if (accountTransfers != null && !accountTransfers.isEmpty()) { - undoTransactions(accountTransfers); - } - - } - - @Override - @Transactional - public void reverseTransfersWithFromAccountTransactions(final Collection fromTransactionIds, - final PortfolioAccountType accountTypeId) { - List accountTransfers = new ArrayList<>(); - if (accountTypeId.isLoanAccount()) { - List> partitions = Lists.partition(fromTransactionIds.stream().toList(), - fineractProperties.getQuery().getInClauseParameterSizeLimit()); - partitions.forEach(partition -> accountTransfers.addAll(this.accountTransferRepository.findByFromLoanTransactions(partition))); - } - if (!accountTransfers.isEmpty()) { - undoTransactions(accountTransfers); - } - } - - @Override - @Transactional - public void reverseAllTransactions(final Long accountId, final PortfolioAccountType accountTypeId) { - List accountTransfers = null; - if (accountTypeId.isLoanAccount()) { - accountTransfers = this.accountTransferRepository.findAllByLoanId(accountId); - } - if (accountTransfers != null && !accountTransfers.isEmpty()) { - undoTransactions(accountTransfers); - } - } - - private void undoTransactions(final List accountTransfers) { - for (final AccountTransferTransaction accountTransfer : accountTransfers) { - if (accountTransfer.getFromLoanTransaction() != null) { - this.loanAccountDomainService.reverseTransfer(accountTransfer.getFromLoanTransaction()); - } - if (accountTransfer.getToLoanTransaction() != null) { - this.loanAccountDomainService.reverseTransfer(accountTransfer.getToLoanTransaction()); - } - if (accountTransfer.getFromTransaction() != null) { - this.savingsAccountWritePlatformService.undoTransaction( - accountTransfer.accountTransferDetails().fromSavingsAccount().getId(), accountTransfer.getFromTransaction().getId(), - true); - } - if (accountTransfer.getToSavingsTransaction() != null) { - this.savingsAccountWritePlatformService.undoTransaction(accountTransfer.accountTransferDetails().toSavingsAccount().getId(), - accountTransfer.getToSavingsTransaction().getId(), true); - } - accountTransfer.reverse(); - this.accountTransferRepository.save(accountTransfer); - } - } - - @Override - @Transactional - public Long transferFunds(final AccountTransferDTO accountTransferDTO) { - Long transferTransactionId = null; - final boolean isAccountTransfer = true; - final boolean isRegularTransaction = accountTransferDTO.isRegularTransaction(); - final boolean backdatedTxnsAllowedTill = false; - AccountTransferDetails accountTransferDetails = accountTransferDTO.getAccountTransferDetails(); - if (isSavingsToLoanAccountTransfer(accountTransferDTO.getFromAccountType(), accountTransferDTO.getToAccountType())) { - // - SavingsAccount fromSavingsAccount = null; - Loan toLoanAccount = null; - if (accountTransferDetails == null) { - if (accountTransferDTO.getFromSavingsAccount() == null) { - fromSavingsAccount = this.savingsAccountAssembler.assembleFrom(accountTransferDTO.getFromAccountId(), - backdatedTxnsAllowedTill); - } else { - fromSavingsAccount = accountTransferDTO.getFromSavingsAccount(); - this.savingsAccountAssembler.setHelpers(fromSavingsAccount); - } - if (accountTransferDTO.getLoan() == null) { - toLoanAccount = this.loanAccountAssembler.assembleFrom(accountTransferDTO.getToAccountId()); - } else { - toLoanAccount = accountTransferDTO.getLoan(); - } - - } else { - fromSavingsAccount = accountTransferDetails.fromSavingsAccount(); - this.savingsAccountAssembler.setHelpers(fromSavingsAccount); - toLoanAccount = accountTransferDetails.toLoanAccount(); - } - - final SavingsTransactionBooleanValues transactionBooleanValues = new SavingsTransactionBooleanValues(isAccountTransfer, - isRegularTransaction, fromSavingsAccount.isWithdrawalFeeApplicableForTransfer(), - AccountTransferType.fromInt(accountTransferDTO.getTransferType()).isInterestTransfer(), - accountTransferDTO.isExceptionForBalanceCheck()); - - final SavingsAccountTransaction withdrawal = this.savingsAccountDomainService.handleWithdrawal(fromSavingsAccount, - accountTransferDTO.getFmt(), accountTransferDTO.getTransactionDate(), accountTransferDTO.getTransactionAmount(), - accountTransferDTO.getPaymentDetail(), transactionBooleanValues, backdatedTxnsAllowedTill); - - LoanTransaction loanTransaction; - - ExternalId txnExternalId = accountTransferDTO.getTxnExternalId(); - // Safety net (it might need to generate new one) - ExternalId externalId = externalIdFactory.create(txnExternalId.getValue()); - - if (AccountTransferType.fromInt(accountTransferDTO.getTransferType()).isChargePayment()) { - loanTransaction = this.loanAccountDomainService.makeChargePayment(toLoanAccount, accountTransferDTO.getChargeId(), - accountTransferDTO.getTransactionDate(), accountTransferDTO.getTransactionAmount(), - accountTransferDTO.getPaymentDetail(), null, externalId, accountTransferDTO.getToTransferType(), - accountTransferDTO.getLoanInstallmentNumber()); - - } else if (AccountTransferType.fromInt(accountTransferDTO.getTransferType()).isLoanDownPayment()) { - final boolean isRecoveryRepayment = false; - final Boolean isHolidayValidationDone = false; - final HolidayDetailDTO holidayDetailDto = null; - final String chargeRefundChargeType = null; - loanTransaction = this.loanAccountDomainService.makeRepayment(LoanTransactionType.DOWN_PAYMENT, toLoanAccount, - accountTransferDTO.getTransactionDate(), accountTransferDTO.getTransactionAmount(), - accountTransferDTO.getPaymentDetail(), null, externalId, isRecoveryRepayment, chargeRefundChargeType, - isAccountTransfer, holidayDetailDto, isHolidayValidationDone); - toLoanAccount = loanTransaction.getLoan(); - } else { - final boolean isRecoveryRepayment = false; - final Boolean isHolidayValidationDone = false; - final HolidayDetailDTO holidayDetailDto = null; - final String chargeRefundChargeType = null; - loanTransaction = this.loanAccountDomainService.makeRepayment(LoanTransactionType.REPAYMENT, toLoanAccount, - accountTransferDTO.getTransactionDate(), accountTransferDTO.getTransactionAmount(), - accountTransferDTO.getPaymentDetail(), null, externalId, isRecoveryRepayment, chargeRefundChargeType, - isAccountTransfer, holidayDetailDto, isHolidayValidationDone); - toLoanAccount = loanTransaction.getLoan(); - } - - accountTransferDetails = this.accountTransferAssembler.assembleSavingsToLoanTransfer(accountTransferDTO, fromSavingsAccount, - toLoanAccount, withdrawal, loanTransaction); - this.accountTransferDetailRepository.saveAndFlush(accountTransferDetails); - transferTransactionId = accountTransferDetails.getId(); - } else if (isSavingsToSavingsAccountTransfer(accountTransferDTO.getFromAccountType(), accountTransferDTO.getToAccountType())) { - - SavingsAccount fromSavingsAccount; - SavingsAccount toSavingsAccount; - if (accountTransferDetails == null) { - if (accountTransferDTO.getFromSavingsAccount() == null) { - fromSavingsAccount = this.savingsAccountAssembler.assembleFrom(accountTransferDTO.getFromAccountId(), - backdatedTxnsAllowedTill); - } else { - fromSavingsAccount = accountTransferDTO.getFromSavingsAccount(); - this.savingsAccountAssembler.setHelpers(fromSavingsAccount); - } - if (accountTransferDTO.getToSavingsAccount() == null) { - toSavingsAccount = this.savingsAccountAssembler.assembleFrom(accountTransferDTO.getToAccountId(), false); - } else { - toSavingsAccount = accountTransferDTO.getToSavingsAccount(); - this.savingsAccountAssembler.setHelpers(toSavingsAccount); - } - } else { - fromSavingsAccount = accountTransferDetails.fromSavingsAccount(); - this.savingsAccountAssembler.setHelpers(fromSavingsAccount); - toSavingsAccount = accountTransferDetails.toSavingsAccount(); - this.savingsAccountAssembler.setHelpers(toSavingsAccount); - } - - final SavingsTransactionBooleanValues transactionBooleanValues = new SavingsTransactionBooleanValues(isAccountTransfer, - isRegularTransaction, fromSavingsAccount.isWithdrawalFeeApplicableForTransfer(), - AccountTransferType.fromInt(accountTransferDTO.getTransferType()).isInterestTransfer(), - accountTransferDTO.isExceptionForBalanceCheck()); - - LocalDate transactionDate = accountTransferDTO.getTransactionDate(); - if (configurationDomainService.isSavingsInterestPostingAtCurrentPeriodEnd() - && configurationDomainService.isNextDayFixedDepositInterestTransferEnabledForPeriodEnd() - && AccountTransferType.fromInt(accountTransferDTO.getTransferType()).isInterestTransfer()) { - transactionDate = transactionDate.plusDays(1); - } - - final SavingsAccountTransaction withdrawal = this.savingsAccountDomainService.handleWithdrawal(fromSavingsAccount, - accountTransferDTO.getFmt(), transactionDate, accountTransferDTO.getTransactionAmount(), - accountTransferDTO.getPaymentDetail(), transactionBooleanValues, backdatedTxnsAllowedTill); - - final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount, - accountTransferDTO.getFmt(), transactionDate, accountTransferDTO.getTransactionAmount(), - accountTransferDTO.getPaymentDetail(), isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill); - - accountTransferDetails = this.accountTransferAssembler.assembleSavingsToSavingsTransfer(accountTransferDTO, fromSavingsAccount, - toSavingsAccount, withdrawal, deposit); - this.accountTransferDetailRepository.saveAndFlush(accountTransferDetails); - transferTransactionId = accountTransferDetails.getId(); - - } else if (isLoanToSavingsAccountTransfer(accountTransferDTO.getFromAccountType(), accountTransferDTO.getToAccountType())) { - - Loan fromLoanAccount = null; - SavingsAccount toSavingsAccount = null; - if (accountTransferDetails == null) { - if (accountTransferDTO.getLoan() == null) { - fromLoanAccount = this.loanAccountAssembler.assembleFrom(accountTransferDTO.getFromAccountId()); - } else { - fromLoanAccount = accountTransferDTO.getLoan(); - } - toSavingsAccount = this.savingsAccountAssembler.assembleFrom(accountTransferDTO.getToAccountId(), backdatedTxnsAllowedTill); - } else { - fromLoanAccount = accountTransferDetails.fromLoanAccount(); - toSavingsAccount = accountTransferDetails.toSavingsAccount(); - this.savingsAccountAssembler.setHelpers(toSavingsAccount); - } - LoanTransaction loanTransaction = null; - - ExternalId txnExternalId = accountTransferDTO.getTxnExternalId(); - // Safety net (it might need to generate new one) - ExternalId externalId = externalIdFactory.create(txnExternalId.getValue()); - - if (LoanTransactionType.DISBURSEMENT.getValue().equals(accountTransferDTO.getFromTransferType())) { - loanTransaction = this.loanAccountDomainService.makeDisburseTransaction(accountTransferDTO.getFromAccountId(), - accountTransferDTO.getTransactionDate(), accountTransferDTO.getTransactionAmount(), - accountTransferDTO.getPaymentDetail(), accountTransferDTO.getNoteText(), externalId); - } else { - loanTransaction = this.loanAccountDomainService.makeRefund(accountTransferDTO.getFromAccountId(), - new CommandProcessingResultBuilder(), accountTransferDTO.getTransactionDate(), - accountTransferDTO.getTransactionAmount(), accountTransferDTO.getPaymentDetail(), accountTransferDTO.getNoteText(), - externalId); - } - - final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount, - accountTransferDTO.getFmt(), accountTransferDTO.getTransactionDate(), accountTransferDTO.getTransactionAmount(), - accountTransferDTO.getPaymentDetail(), isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill); - accountTransferDetails = this.accountTransferAssembler.assembleLoanToSavingsTransfer(accountTransferDTO, fromLoanAccount, - toSavingsAccount, deposit, loanTransaction); - this.accountTransferDetailRepository.saveAndFlush(accountTransferDetails); - transferTransactionId = accountTransferDetails.getId(); - - // if the savings account is GSIM, update its parent as well - if (toSavingsAccount.getGsim() != null) { - GroupSavingsIndividualMonitoring gsim = gsimRepository.findById(toSavingsAccount.getGsim().getId()).orElseThrow(); - BigDecimal currentBalance = gsim.getParentDeposit(); - BigDecimal newBalance = currentBalance.add(accountTransferDTO.getTransactionAmount()); - gsim.setParentDeposit(newBalance); - gsimRepository.save(gsim); - } - } else { - throw new GeneralPlatformDomainRuleException("error.msg.accounttransfer.loan.to.loan.not.supported", - "Account transfer from loan to another loan is not supported"); - } - - return transferTransactionId; - } - - @Override - public AccountTransferDetails repayLoanWithTopup(AccountTransferDTO accountTransferDTO) { - final boolean isAccountTransfer = true; - Loan fromLoanAccount = null; - if (accountTransferDTO.getFromLoan() == null) { - fromLoanAccount = this.loanAccountAssembler.assembleFrom(accountTransferDTO.getFromAccountId()); - } else { - fromLoanAccount = accountTransferDTO.getFromLoan(); - } - Loan toLoanAccount = null; - if (accountTransferDTO.getToLoan() == null) { - toLoanAccount = this.loanAccountAssembler.assembleFrom(accountTransferDTO.getToAccountId()); - } else { - toLoanAccount = accountTransferDTO.getToLoan(); - } - - ExternalId externalIdForDisbursement = accountTransferDTO.getTxnExternalId(); - - LoanTransaction disburseTransaction = this.loanAccountDomainService.makeDisburseTransaction(accountTransferDTO.getFromAccountId(), - accountTransferDTO.getTransactionDate(), accountTransferDTO.getTransactionAmount(), accountTransferDTO.getPaymentDetail(), - accountTransferDTO.getNoteText(), externalIdForDisbursement, true); - final String chargeRefundChargeType = null; - - ExternalId externalIdForRepayment = externalIdFactory.create(); - - LoanTransaction repayTransaction = this.loanAccountDomainService.makeRepayment(LoanTransactionType.REPAYMENT, toLoanAccount, - accountTransferDTO.getTransactionDate(), accountTransferDTO.getTransactionAmount(), accountTransferDTO.getPaymentDetail(), - null, externalIdForRepayment, false, chargeRefundChargeType, isAccountTransfer, null, false, true); - - AccountTransferDetails accountTransferDetails = this.accountTransferAssembler.assembleLoanToLoanTransfer(accountTransferDTO, - fromLoanAccount, toLoanAccount, disburseTransaction, repayTransaction); - this.accountTransferDetailRepository.saveAndFlush(accountTransferDetails); - - return accountTransferDetails; - } - - private boolean isLoanToSavingsAccountTransfer(final PortfolioAccountType fromAccountType, final PortfolioAccountType toAccountType) { - return fromAccountType.isLoanAccount() && toAccountType.isSavingsAccount(); - } - - private boolean isSavingsToLoanAccountTransfer(final PortfolioAccountType fromAccountType, final PortfolioAccountType toAccountType) { - return fromAccountType.isSavingsAccount() && toAccountType.isLoanAccount(); - } - - private boolean isSavingsToSavingsAccountTransfer(final PortfolioAccountType fromAccountType, - final PortfolioAccountType toAccountType) { - return fromAccountType.isSavingsAccount() && toAccountType.isSavingsAccount(); - } - - @Override - @Transactional - public CommandProcessingResult refundByTransfer(JsonCommand command) { - // TODO Auto-generated method stub - this.accountTransfersDataValidator.validate(command); - - final LocalDate transactionDate = command.localDateValueOfParameterNamed(transferDateParamName); - final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed(transferAmountParamName); - - final Locale locale = command.extractLocale(); - final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); - - final PaymentDetail paymentDetail = null; - Long transferTransactionId = null; - - final Long fromLoanAccountId = command.longValueOfParameterNamed(fromAccountIdParamName); - final Loan fromLoanAccount = this.loanAccountAssembler.assembleFrom(fromLoanAccountId); - - BigDecimal overpaid = this.loanReadPlatformService.retrieveTotalPaidInAdvance(fromLoanAccountId).getPaidInAdvance(); - final boolean backdatedTxnsAllowedTill = false; - - if (overpaid == null || overpaid.compareTo(BigDecimal.ZERO) == 0 || transactionAmount.floatValue() > overpaid.floatValue()) { - if (overpaid == null) { - overpaid = BigDecimal.ZERO; - } - throw new InvalidPaidInAdvanceAmountException(overpaid.toPlainString()); - } - - ExternalId externalId = externalIdFactory.create(); - - final LoanTransaction loanRefundTransaction = this.loanAccountDomainService.makeRefundForActiveLoan(fromLoanAccountId, - new CommandProcessingResultBuilder(), transactionDate, transactionAmount, paymentDetail, null, externalId); - - final Long toSavingsAccountId = command.longValueOfParameterNamed(toAccountIdParamName); - final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsAccountId, backdatedTxnsAllowedTill); - - final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount, fmt, transactionDate, - transactionAmount, paymentDetail, true, true, backdatedTxnsAllowedTill); - - final AccountTransferDetails accountTransferDetails = this.accountTransferAssembler.assembleLoanToSavingsTransfer(command, - fromLoanAccount, toSavingsAccount, deposit, loanRefundTransaction); - this.accountTransferDetailRepository.saveAndFlush(accountTransferDetails); - transferTransactionId = accountTransferDetails.getId(); - - final CommandProcessingResultBuilder builder = new CommandProcessingResultBuilder().withEntityId(transferTransactionId); - - // if (fromAccountType.isSavingsAccount()) { - - builder.withSavingsId(toSavingsAccountId); - // } - - return builder.build(); - } -} +/** + * 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.account.service; + +import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromAccountIdParamName; +import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromAccountTypeParamName; +import static org.apache.fineract.portfolio.account.AccountDetailConstants.toAccountIdParamName; +import static org.apache.fineract.portfolio.account.AccountDetailConstants.toAccountTypeParamName; +import static org.apache.fineract.portfolio.account.api.AccountTransfersApiConstants.transferAmountParamName; +import static org.apache.fineract.portfolio.account.api.AccountTransfersApiConstants.transferDateParamName; + +import com.google.common.collect.Lists; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.config.FineractProperties; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; +import org.apache.fineract.infrastructure.core.domain.ExternalId; +import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException; +import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; +import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.portfolio.account.PortfolioAccountType; +import org.apache.fineract.portfolio.account.data.AccountTransferDTO; +import org.apache.fineract.portfolio.account.data.AccountTransfersDataValidator; +import org.apache.fineract.portfolio.account.domain.AccountTransferAssembler; +import org.apache.fineract.portfolio.account.domain.AccountTransferDetailRepository; +import org.apache.fineract.portfolio.account.domain.AccountTransferDetails; +import org.apache.fineract.portfolio.account.domain.AccountTransferRepository; +import org.apache.fineract.portfolio.account.domain.AccountTransferTransaction; +import org.apache.fineract.portfolio.account.domain.AccountTransferType; +import org.apache.fineract.portfolio.account.exception.DifferentCurrenciesException; +import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccount; +import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountRepository; +import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountTransaction; +import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountTransactionRepository; +import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; +import org.apache.fineract.portfolio.loanaccount.exception.InvalidPaidInAdvanceAmountException; +import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler; +import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService; +import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail; +import org.apache.fineract.portfolio.savings.SavingsTransactionBooleanValues; +import org.apache.fineract.portfolio.savings.domain.GSIMRepositoy; +import org.apache.fineract.portfolio.savings.domain.GroupSavingsIndividualMonitoring; +import org.apache.fineract.portfolio.savings.domain.SavingsAccount; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction; +import org.apache.fineract.portfolio.savings.service.SavingsAccountDomainService; +import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +public class AccountTransfersWritePlatformServiceImpl implements AccountTransfersWritePlatformService { + + private final AccountTransfersDataValidator accountTransfersDataValidator; + private final AccountTransferAssembler accountTransferAssembler; + private final AccountTransferRepository accountTransferRepository; + private final SavingsAccountAssembler savingsAccountAssembler; + private final SavingsAccountDomainService savingsAccountDomainService; + private final LoanAssembler loanAccountAssembler; + private final LoanAccountDomainService loanAccountDomainService; + private final SavingsAccountWritePlatformService savingsAccountWritePlatformService; + private final AccountTransferDetailRepository accountTransferDetailRepository; + private final LoanReadPlatformService loanReadPlatformService; + private final GSIMRepositoy gsimRepository; + private final ConfigurationDomainService configurationDomainService; + private final ExternalIdFactory externalIdFactory; + private final FineractProperties fineractProperties; + private final CurrentAccountRepository currentAccountRepository; + private final CurrentAccountTransactionRepository currentAccountTransactionRepository; + + @Transactional + @Override + public CommandProcessingResult create(final JsonCommand command) { + boolean isRegularTransaction = true; + + this.accountTransfersDataValidator.validate(command); + + final LocalDate transactionDate = command.localDateValueOfParameterNamed(transferDateParamName); + final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed(transferAmountParamName); + + final Locale locale = command.extractLocale(); + final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); + + final Integer fromAccountTypeId = command.integerValueSansLocaleOfParameterNamed(fromAccountTypeParamName); + final PortfolioAccountType fromAccountType = PortfolioAccountType.fromInt(fromAccountTypeId); + + final Integer toAccountTypeId = command.integerValueSansLocaleOfParameterNamed(toAccountTypeParamName); + final PortfolioAccountType toAccountType = PortfolioAccountType.fromInt(toAccountTypeId); + + final PaymentDetail paymentDetail = null; + Long fromSavingsAccountId = null; + Long transferDetailId = null; + boolean isInterestTransfer = false; + boolean isAccountTransfer = true; + Long fromLoanAccountId = null; + boolean isWithdrawBalance = false; + final boolean backdatedTxnsAllowedTill = false; + + if (isSavingsToSavingsAccountTransfer(fromAccountType, toAccountType)) { + + fromSavingsAccountId = command.longValueOfParameterNamed(fromAccountIdParamName); + final SavingsAccount fromSavingsAccount = this.savingsAccountAssembler.assembleFrom(fromSavingsAccountId, + backdatedTxnsAllowedTill); + + final SavingsTransactionBooleanValues transactionBooleanValues = new SavingsTransactionBooleanValues(isAccountTransfer, + isRegularTransaction, fromSavingsAccount.isWithdrawalFeeApplicableForTransfer(), isInterestTransfer, isWithdrawBalance); + final SavingsAccountTransaction withdrawal = this.savingsAccountDomainService.handleWithdrawal(fromSavingsAccount, fmt, + transactionDate, transactionAmount, paymentDetail, transactionBooleanValues, backdatedTxnsAllowedTill); + + final Long toSavingsId = command.longValueOfParameterNamed(toAccountIdParamName); + final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsId, backdatedTxnsAllowedTill); + + final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount, fmt, transactionDate, + transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill); + + if (!fromSavingsAccount.getCurrency().getCode().equals(toSavingsAccount.getCurrency().getCode())) { + throw new DifferentCurrenciesException(fromSavingsAccount.getCurrency().getCode(), + toSavingsAccount.getCurrency().getCode()); + } + + final AccountTransferDetails accountTransferDetails = this.accountTransferAssembler.assembleSavingsToSavingsTransfer(command, + fromSavingsAccount, toSavingsAccount, withdrawal, deposit); + this.accountTransferDetailRepository.saveAndFlush(accountTransferDetails); + transferDetailId = accountTransferDetails.getId(); + + } else if (isSavingsToLoanAccountTransfer(fromAccountType, toAccountType)) { + // + fromSavingsAccountId = command.longValueOfParameterNamed(fromAccountIdParamName); + final SavingsAccount fromSavingsAccount = this.savingsAccountAssembler.assembleFrom(fromSavingsAccountId, + backdatedTxnsAllowedTill); + + final SavingsTransactionBooleanValues transactionBooleanValues = new SavingsTransactionBooleanValues(isAccountTransfer, + isRegularTransaction, fromSavingsAccount.isWithdrawalFeeApplicableForTransfer(), isInterestTransfer, isWithdrawBalance); + final SavingsAccountTransaction withdrawal = this.savingsAccountDomainService.handleWithdrawal(fromSavingsAccount, fmt, + transactionDate, transactionAmount, paymentDetail, transactionBooleanValues, backdatedTxnsAllowedTill); + + final Long toLoanAccountId = command.longValueOfParameterNamed(toAccountIdParamName); + Loan toLoanAccount = this.loanAccountAssembler.assembleFrom(toLoanAccountId); + + final Boolean isHolidayValidationDone = false; + final HolidayDetailDTO holidayDetailDto = null; + final boolean isRecoveryRepayment = false; + final String chargeRefundChargeType = null; + + ExternalId externalId = externalIdFactory.create(); + final LoanTransaction loanRepaymentTransaction = this.loanAccountDomainService.makeRepayment(LoanTransactionType.REPAYMENT, + toLoanAccount, transactionDate, transactionAmount, paymentDetail, null, externalId, isRecoveryRepayment, + chargeRefundChargeType, isAccountTransfer, holidayDetailDto, isHolidayValidationDone); + toLoanAccount = loanRepaymentTransaction.getLoan(); + final AccountTransferDetails accountTransferDetails = this.accountTransferAssembler.assembleSavingsToLoanTransfer(command, + fromSavingsAccount, toLoanAccount, withdrawal, loanRepaymentTransaction); + this.accountTransferDetailRepository.saveAndFlush(accountTransferDetails); + transferDetailId = accountTransferDetails.getId(); + + } else if (isLoanToSavingsAccountTransfer(fromAccountType, toAccountType)) { + // FIXME - kw - ADD overpaid loan to savings account transfer + // support. + + fromLoanAccountId = command.longValueOfParameterNamed(fromAccountIdParamName); + final Loan fromLoanAccount = this.loanAccountAssembler.assembleFrom(fromLoanAccountId); + ExternalId externalId = externalIdFactory.create(); + final LoanTransaction loanRefundTransaction = this.loanAccountDomainService.makeRefund(fromLoanAccountId, + new CommandProcessingResultBuilder(), transactionDate, transactionAmount, paymentDetail, null, externalId); + + final Long toSavingsAccountId = command.longValueOfParameterNamed(toAccountIdParamName); + final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsAccountId, backdatedTxnsAllowedTill); + + final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount, fmt, transactionDate, + transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill); + + final AccountTransferDetails accountTransferDetails = this.accountTransferAssembler.assembleLoanToSavingsTransfer(command, + fromLoanAccount, toSavingsAccount, deposit, loanRefundTransaction); + this.accountTransferDetailRepository.saveAndFlush(accountTransferDetails); + transferDetailId = accountTransferDetails.getId(); + + } + + final CommandProcessingResultBuilder builder = new CommandProcessingResultBuilder().withEntityId(transferDetailId); + + if (fromAccountType.isSavingsAccount()) { + builder.withSavingsId(fromSavingsAccountId); + } + if (fromAccountType.isLoanAccount()) { + builder.withLoanId(fromLoanAccountId); + } + + return builder.build(); + } + + @Override + @Transactional + public void reverseTransfersWithFromAccountType(final Long accountNumber, final PortfolioAccountType accountTypeId) { + List accountTransfers = null; + if (accountTypeId.isLoanAccount()) { + accountTransfers = this.accountTransferRepository.findByFromLoanId(accountNumber); + } + if (accountTransfers != null && !accountTransfers.isEmpty()) { + undoTransactions(accountTransfers); + } + + } + + @Override + @Transactional + public void reverseTransfersWithFromAccountTransactions(final Collection fromTransactionIds, + final PortfolioAccountType accountTypeId) { + List accountTransfers = new ArrayList<>(); + if (accountTypeId.isLoanAccount()) { + List> partitions = Lists.partition(fromTransactionIds.stream().toList(), + fineractProperties.getQuery().getInClauseParameterSizeLimit()); + partitions.forEach(partition -> accountTransfers.addAll(this.accountTransferRepository.findByFromLoanTransactions(partition))); + } + if (!accountTransfers.isEmpty()) { + undoTransactions(accountTransfers); + } + } + + @Override + @Transactional + public void reverseAllTransactions(final Long accountId, final PortfolioAccountType accountTypeId) { + List accountTransfers = null; + if (accountTypeId.isLoanAccount()) { + accountTransfers = this.accountTransferRepository.findAllByLoanId(accountId); + } + if (accountTransfers != null && !accountTransfers.isEmpty()) { + undoTransactions(accountTransfers); + } + } + + private void undoTransactions(final List accountTransfers) { + for (final AccountTransferTransaction accountTransfer : accountTransfers) { + if (accountTransfer.getFromLoanTransaction() != null) { + this.loanAccountDomainService.reverseTransfer(accountTransfer.getFromLoanTransaction()); + } + if (accountTransfer.getToLoanTransaction() != null) { + this.loanAccountDomainService.reverseTransfer(accountTransfer.getToLoanTransaction()); + } + if (accountTransfer.getFromTransaction() != null) { + this.savingsAccountWritePlatformService.undoTransaction( + accountTransfer.accountTransferDetails().fromSavingsAccount().getId(), accountTransfer.getFromTransaction().getId(), + true); + } + if (accountTransfer.getToSavingsTransaction() != null) { + this.savingsAccountWritePlatformService.undoTransaction(accountTransfer.accountTransferDetails().toSavingsAccount().getId(), + accountTransfer.getToSavingsTransaction().getId(), true); + } + accountTransfer.reverse(); + this.accountTransferRepository.save(accountTransfer); + } + } + + @Override + @Transactional + public Long transferFunds(final AccountTransferDTO accountTransferDTO) { + Long transferTransactionId = null; + final boolean isAccountTransfer = true; + final boolean isRegularTransaction = accountTransferDTO.isRegularTransaction(); + final boolean backdatedTxnsAllowedTill = false; + AccountTransferDetails accountTransferDetails = accountTransferDTO.getAccountTransferDetails(); + if (isSavingsToLoanAccountTransfer(accountTransferDTO.getFromAccountType(), accountTransferDTO.getToAccountType())) { + // + SavingsAccount fromSavingsAccount = null; + Loan toLoanAccount = null; + if (accountTransferDetails == null) { + if (accountTransferDTO.getFromSavingsAccount() == null) { + fromSavingsAccount = this.savingsAccountAssembler.assembleFrom(accountTransferDTO.getFromAccountId(), + backdatedTxnsAllowedTill); + } else { + fromSavingsAccount = accountTransferDTO.getFromSavingsAccount(); + this.savingsAccountAssembler.setHelpers(fromSavingsAccount); + } + if (accountTransferDTO.getLoan() == null) { + toLoanAccount = this.loanAccountAssembler.assembleFrom(accountTransferDTO.getToAccountId()); + } else { + toLoanAccount = accountTransferDTO.getLoan(); + } + + } else { + fromSavingsAccount = accountTransferDetails.fromSavingsAccount(); + this.savingsAccountAssembler.setHelpers(fromSavingsAccount); + toLoanAccount = accountTransferDetails.toLoanAccount(); + } + + final SavingsTransactionBooleanValues transactionBooleanValues = new SavingsTransactionBooleanValues(isAccountTransfer, + isRegularTransaction, fromSavingsAccount.isWithdrawalFeeApplicableForTransfer(), + AccountTransferType.fromInt(accountTransferDTO.getTransferType()).isInterestTransfer(), + accountTransferDTO.isExceptionForBalanceCheck()); + + final SavingsAccountTransaction withdrawal = this.savingsAccountDomainService.handleWithdrawal(fromSavingsAccount, + accountTransferDTO.getFmt(), accountTransferDTO.getTransactionDate(), accountTransferDTO.getTransactionAmount(), + accountTransferDTO.getPaymentDetail(), transactionBooleanValues, backdatedTxnsAllowedTill); + + LoanTransaction loanTransaction; + + ExternalId txnExternalId = accountTransferDTO.getTxnExternalId(); + // Safety net (it might need to generate new one) + ExternalId externalId = externalIdFactory.create(txnExternalId.getValue()); + + if (AccountTransferType.fromInt(accountTransferDTO.getTransferType()).isChargePayment()) { + loanTransaction = this.loanAccountDomainService.makeChargePayment(toLoanAccount, accountTransferDTO.getChargeId(), + accountTransferDTO.getTransactionDate(), accountTransferDTO.getTransactionAmount(), + accountTransferDTO.getPaymentDetail(), null, externalId, accountTransferDTO.getToTransferType(), + accountTransferDTO.getLoanInstallmentNumber()); + + } else if (AccountTransferType.fromInt(accountTransferDTO.getTransferType()).isLoanDownPayment()) { + final boolean isRecoveryRepayment = false; + final Boolean isHolidayValidationDone = false; + final HolidayDetailDTO holidayDetailDto = null; + final String chargeRefundChargeType = null; + loanTransaction = this.loanAccountDomainService.makeRepayment(LoanTransactionType.DOWN_PAYMENT, toLoanAccount, + accountTransferDTO.getTransactionDate(), accountTransferDTO.getTransactionAmount(), + accountTransferDTO.getPaymentDetail(), null, externalId, isRecoveryRepayment, chargeRefundChargeType, + isAccountTransfer, holidayDetailDto, isHolidayValidationDone); + toLoanAccount = loanTransaction.getLoan(); + } else { + final boolean isRecoveryRepayment = false; + final Boolean isHolidayValidationDone = false; + final HolidayDetailDTO holidayDetailDto = null; + final String chargeRefundChargeType = null; + loanTransaction = this.loanAccountDomainService.makeRepayment(LoanTransactionType.REPAYMENT, toLoanAccount, + accountTransferDTO.getTransactionDate(), accountTransferDTO.getTransactionAmount(), + accountTransferDTO.getPaymentDetail(), null, externalId, isRecoveryRepayment, chargeRefundChargeType, + isAccountTransfer, holidayDetailDto, isHolidayValidationDone); + toLoanAccount = loanTransaction.getLoan(); + } + + accountTransferDetails = this.accountTransferAssembler.assembleSavingsToLoanTransfer(accountTransferDTO, fromSavingsAccount, + toLoanAccount, withdrawal, loanTransaction); + this.accountTransferDetailRepository.saveAndFlush(accountTransferDetails); + transferTransactionId = accountTransferDetails.getId(); + } else if (isSavingsToSavingsAccountTransfer(accountTransferDTO.getFromAccountType(), accountTransferDTO.getToAccountType())) { + + SavingsAccount fromSavingsAccount; + SavingsAccount toSavingsAccount; + if (accountTransferDetails == null) { + if (accountTransferDTO.getFromSavingsAccount() == null) { + fromSavingsAccount = this.savingsAccountAssembler.assembleFrom(accountTransferDTO.getFromAccountId(), + backdatedTxnsAllowedTill); + } else { + fromSavingsAccount = accountTransferDTO.getFromSavingsAccount(); + this.savingsAccountAssembler.setHelpers(fromSavingsAccount); + } + if (accountTransferDTO.getToSavingsAccount() == null) { + toSavingsAccount = this.savingsAccountAssembler.assembleFrom(accountTransferDTO.getToAccountId(), false); + } else { + toSavingsAccount = accountTransferDTO.getToSavingsAccount(); + this.savingsAccountAssembler.setHelpers(toSavingsAccount); + } + } else { + fromSavingsAccount = accountTransferDetails.fromSavingsAccount(); + this.savingsAccountAssembler.setHelpers(fromSavingsAccount); + toSavingsAccount = accountTransferDetails.toSavingsAccount(); + this.savingsAccountAssembler.setHelpers(toSavingsAccount); + } + + final SavingsTransactionBooleanValues transactionBooleanValues = new SavingsTransactionBooleanValues(isAccountTransfer, + isRegularTransaction, fromSavingsAccount.isWithdrawalFeeApplicableForTransfer(), + AccountTransferType.fromInt(accountTransferDTO.getTransferType()).isInterestTransfer(), + accountTransferDTO.isExceptionForBalanceCheck()); + + LocalDate transactionDate = accountTransferDTO.getTransactionDate(); + if (configurationDomainService.isSavingsInterestPostingAtCurrentPeriodEnd() + && configurationDomainService.isNextDayFixedDepositInterestTransferEnabledForPeriodEnd() + && AccountTransferType.fromInt(accountTransferDTO.getTransferType()).isInterestTransfer()) { + transactionDate = transactionDate.plusDays(1); + } + + final SavingsAccountTransaction withdrawal = this.savingsAccountDomainService.handleWithdrawal(fromSavingsAccount, + accountTransferDTO.getFmt(), transactionDate, accountTransferDTO.getTransactionAmount(), + accountTransferDTO.getPaymentDetail(), transactionBooleanValues, backdatedTxnsAllowedTill); + + final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount, + accountTransferDTO.getFmt(), transactionDate, accountTransferDTO.getTransactionAmount(), + accountTransferDTO.getPaymentDetail(), isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill); + + accountTransferDetails = this.accountTransferAssembler.assembleSavingsToSavingsTransfer(accountTransferDTO, fromSavingsAccount, + toSavingsAccount, withdrawal, deposit); + this.accountTransferDetailRepository.saveAndFlush(accountTransferDetails); + transferTransactionId = accountTransferDetails.getId(); + + } else if (isLoanToSavingsAccountTransfer(accountTransferDTO.getFromAccountType(), accountTransferDTO.getToAccountType())) { + + Loan fromLoanAccount = null; + SavingsAccount toSavingsAccount = null; + if (accountTransferDetails == null) { + if (accountTransferDTO.getLoan() == null) { + fromLoanAccount = this.loanAccountAssembler.assembleFrom(accountTransferDTO.getFromAccountId()); + } else { + fromLoanAccount = accountTransferDTO.getLoan(); + } + toSavingsAccount = this.savingsAccountAssembler.assembleFrom(accountTransferDTO.getToAccountId(), backdatedTxnsAllowedTill); + } else { + fromLoanAccount = accountTransferDetails.fromLoanAccount(); + toSavingsAccount = accountTransferDetails.toSavingsAccount(); + this.savingsAccountAssembler.setHelpers(toSavingsAccount); + } + LoanTransaction loanTransaction = null; + + ExternalId txnExternalId = accountTransferDTO.getTxnExternalId(); + // Safety net (it might need to generate new one) + ExternalId externalId = externalIdFactory.create(txnExternalId.getValue()); + + if (LoanTransactionType.DISBURSEMENT.getValue().equals(accountTransferDTO.getFromTransferType())) { + loanTransaction = this.loanAccountDomainService.makeDisburseTransaction(accountTransferDTO.getFromAccountId(), + accountTransferDTO.getTransactionDate(), accountTransferDTO.getTransactionAmount(), + accountTransferDTO.getPaymentDetail(), accountTransferDTO.getNoteText(), externalId); + } else { + loanTransaction = this.loanAccountDomainService.makeRefund(accountTransferDTO.getFromAccountId(), + new CommandProcessingResultBuilder(), accountTransferDTO.getTransactionDate(), + accountTransferDTO.getTransactionAmount(), accountTransferDTO.getPaymentDetail(), accountTransferDTO.getNoteText(), + externalId); + } + + final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount, + accountTransferDTO.getFmt(), accountTransferDTO.getTransactionDate(), accountTransferDTO.getTransactionAmount(), + accountTransferDTO.getPaymentDetail(), isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill); + accountTransferDetails = this.accountTransferAssembler.assembleLoanToSavingsTransfer(accountTransferDTO, fromLoanAccount, + toSavingsAccount, deposit, loanTransaction); + this.accountTransferDetailRepository.saveAndFlush(accountTransferDetails); + transferTransactionId = accountTransferDetails.getId(); + + // if the savings account is GSIM, update its parent as well + if (toSavingsAccount.getGsim() != null) { + GroupSavingsIndividualMonitoring gsim = gsimRepository.findById(toSavingsAccount.getGsim().getId()).orElseThrow(); + BigDecimal currentBalance = gsim.getParentDeposit(); + BigDecimal newBalance = currentBalance.add(accountTransferDTO.getTransactionAmount()); + gsim.setParentDeposit(newBalance); + gsimRepository.save(gsim); + } + + } else if (isCurrentToSavingsAccountTransfer(accountTransferDTO.getFromAccountType(), accountTransferDTO.getToAccountType())) { + // --- Current Account → Savings Account --- + final CurrentAccount fromCurrentAccount = currentAccountRepository + .findById(accountTransferDTO.getFromAccountId()) + .orElseThrow(() -> new GeneralPlatformDomainRuleException( + "error.msg.currentaccount.not.found", "Current account not found: " + accountTransferDTO.getFromAccountId())); + final SavingsAccount toSavingsAccount = this.savingsAccountAssembler + .assembleFrom(accountTransferDTO.getToAccountId(), backdatedTxnsAllowedTill); + + final CurrentAccountTransaction withdrawal = performCurrentWithdrawal(fromCurrentAccount, + accountTransferDTO.getTransactionAmount(), accountTransferDTO.getTransactionDate()); + + final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount, + accountTransferDTO.getFmt(), accountTransferDTO.getTransactionDate(), + accountTransferDTO.getTransactionAmount(), accountTransferDTO.getPaymentDetail(), + isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill); + + final AccountTransferDetails newDetails = AccountTransferDetails.currentToSavingsTransfer( + fromCurrentAccount.getOffice(), fromCurrentAccount.getClient(), fromCurrentAccount, + toSavingsAccount.office(), toSavingsAccount.getClient(), toSavingsAccount, + accountTransferDTO.getTransferType()); + final Money amount = Money.of(toSavingsAccount.getCurrency(), accountTransferDTO.getTransactionAmount()); + newDetails.addAccountTransferTransaction(AccountTransferTransaction.savingsToSavingsTransfer( + newDetails, null, deposit, accountTransferDTO.getTransactionDate(), amount, + accountTransferDTO.getDescription())); + this.accountTransferDetailRepository.saveAndFlush(newDetails); + transferTransactionId = newDetails.getId(); + + } else if (isSavingsToCurrentAccountTransfer(accountTransferDTO.getFromAccountType(), accountTransferDTO.getToAccountType())) { + // --- Savings Account → Current Account --- + final SavingsAccount fromSavingsAccount = this.savingsAccountAssembler + .assembleFrom(accountTransferDTO.getFromAccountId(), backdatedTxnsAllowedTill); + final CurrentAccount toCurrentAccount = currentAccountRepository + .findById(accountTransferDTO.getToAccountId()) + .orElseThrow(() -> new GeneralPlatformDomainRuleException( + "error.msg.currentaccount.not.found", "Current account not found: " + accountTransferDTO.getToAccountId())); + + final SavingsTransactionBooleanValues txnBooleans = new SavingsTransactionBooleanValues(isAccountTransfer, + isRegularTransaction, fromSavingsAccount.isWithdrawalFeeApplicableForTransfer(), false, + accountTransferDTO.isExceptionForBalanceCheck()); + final SavingsAccountTransaction withdrawal = this.savingsAccountDomainService.handleWithdrawal(fromSavingsAccount, + accountTransferDTO.getFmt(), accountTransferDTO.getTransactionDate(), + accountTransferDTO.getTransactionAmount(), accountTransferDTO.getPaymentDetail(), + txnBooleans, backdatedTxnsAllowedTill); + + performCurrentDeposit(toCurrentAccount, accountTransferDTO.getTransactionAmount(), + accountTransferDTO.getTransactionDate()); + + final AccountTransferDetails newDetails = AccountTransferDetails.savingsToCurrentTransfer( + fromSavingsAccount.office(), fromSavingsAccount.getClient(), fromSavingsAccount, + toCurrentAccount.getOffice(), toCurrentAccount.getClient(), toCurrentAccount, + accountTransferDTO.getTransferType()); + final Money amount = Money.of(fromSavingsAccount.getCurrency(), accountTransferDTO.getTransactionAmount()); + newDetails.addAccountTransferTransaction(AccountTransferTransaction.savingsToSavingsTransfer( + newDetails, withdrawal, null, accountTransferDTO.getTransactionDate(), amount, + accountTransferDTO.getDescription())); + this.accountTransferDetailRepository.saveAndFlush(newDetails); + transferTransactionId = newDetails.getId(); + + } else if (isCurrentToCurrentAccountTransfer(accountTransferDTO.getFromAccountType(), accountTransferDTO.getToAccountType())) { + // --- Current Account → Current Account --- + final CurrentAccount fromCurrentAccount = currentAccountRepository + .findById(accountTransferDTO.getFromAccountId()) + .orElseThrow(() -> new GeneralPlatformDomainRuleException( + "error.msg.currentaccount.not.found", "Current account not found: " + accountTransferDTO.getFromAccountId())); + final CurrentAccount toCurrentAccount = currentAccountRepository + .findById(accountTransferDTO.getToAccountId()) + .orElseThrow(() -> new GeneralPlatformDomainRuleException( + "error.msg.currentaccount.not.found", "Current account not found: " + accountTransferDTO.getToAccountId())); + + performCurrentWithdrawal(fromCurrentAccount, accountTransferDTO.getTransactionAmount(), + accountTransferDTO.getTransactionDate()); + performCurrentDeposit(toCurrentAccount, accountTransferDTO.getTransactionAmount(), + accountTransferDTO.getTransactionDate()); + + final AccountTransferDetails newDetails = AccountTransferDetails.currentToCurrentTransfer( + fromCurrentAccount.getOffice(), fromCurrentAccount.getClient(), fromCurrentAccount, + toCurrentAccount.getOffice(), toCurrentAccount.getClient(), toCurrentAccount, + accountTransferDTO.getTransferType()); + final MonetaryCurrency currency = new MonetaryCurrency(fromCurrentAccount.getCurrencyCode(), + fromCurrentAccount.getCurrencyDigits(), fromCurrentAccount.getCurrencyMultiplesOf()); + final Money amount = Money.of(currency, accountTransferDTO.getTransactionAmount()); + newDetails.addAccountTransferTransaction(AccountTransferTransaction.savingsToSavingsTransfer( + newDetails, null, null, accountTransferDTO.getTransactionDate(), amount, + accountTransferDTO.getDescription())); + this.accountTransferDetailRepository.saveAndFlush(newDetails); + transferTransactionId = newDetails.getId(); + + } else if (isCurrentToLoanAccountTransfer(accountTransferDTO.getFromAccountType(), accountTransferDTO.getToAccountType())) { + // --- Current Account → Loan Account --- + final CurrentAccount fromCurrentAccount = currentAccountRepository + .findById(accountTransferDTO.getFromAccountId()) + .orElseThrow(() -> new GeneralPlatformDomainRuleException( + "error.msg.currentaccount.not.found", "Current account not found: " + accountTransferDTO.getFromAccountId())); + final Loan toLoanAccount = this.loanAccountAssembler.assembleFrom(accountTransferDTO.getToAccountId()); + + performCurrentWithdrawal(fromCurrentAccount, accountTransferDTO.getTransactionAmount(), + accountTransferDTO.getTransactionDate()); + + ExternalId externalId = externalIdFactory.create(accountTransferDTO.getTxnExternalId().getValue()); + final boolean isRecoveryRepayment = false; + final Boolean isHolidayValidationDone = false; + final String chargeRefundChargeType = null; + LoanTransaction loanTransaction = this.loanAccountDomainService.makeRepayment(LoanTransactionType.REPAYMENT, + toLoanAccount, accountTransferDTO.getTransactionDate(), accountTransferDTO.getTransactionAmount(), + accountTransferDTO.getPaymentDetail(), null, externalId, isRecoveryRepayment, + chargeRefundChargeType, isAccountTransfer, null, isHolidayValidationDone); + + final AccountTransferDetails newDetails = AccountTransferDetails.currentToLoanTransfer( + fromCurrentAccount.getOffice(), fromCurrentAccount.getClient(), fromCurrentAccount, + toLoanAccount.getOffice(), toLoanAccount.client(), toLoanAccount, + accountTransferDTO.getTransferType()); + final MonetaryCurrency currency = new MonetaryCurrency(fromCurrentAccount.getCurrencyCode(), + fromCurrentAccount.getCurrencyDigits(), fromCurrentAccount.getCurrencyMultiplesOf()); + final Money amount = Money.of(currency, accountTransferDTO.getTransactionAmount()); + newDetails.addAccountTransferTransaction(AccountTransferTransaction.savingsToLoanTransfer( + newDetails, null, loanTransaction, accountTransferDTO.getTransactionDate(), amount, + accountTransferDTO.getDescription())); + this.accountTransferDetailRepository.saveAndFlush(newDetails); + transferTransactionId = newDetails.getId(); + + } else { + throw new GeneralPlatformDomainRuleException("error.msg.accounttransfer.loan.to.loan.not.supported", + "Account transfer from loan to another loan is not supported"); + } + + return transferTransactionId; + } + + @Override + public AccountTransferDetails repayLoanWithTopup(AccountTransferDTO accountTransferDTO) { + final boolean isAccountTransfer = true; + Loan fromLoanAccount = null; + if (accountTransferDTO.getFromLoan() == null) { + fromLoanAccount = this.loanAccountAssembler.assembleFrom(accountTransferDTO.getFromAccountId()); + } else { + fromLoanAccount = accountTransferDTO.getFromLoan(); + } + Loan toLoanAccount = null; + if (accountTransferDTO.getToLoan() == null) { + toLoanAccount = this.loanAccountAssembler.assembleFrom(accountTransferDTO.getToAccountId()); + } else { + toLoanAccount = accountTransferDTO.getToLoan(); + } + + ExternalId externalIdForDisbursement = accountTransferDTO.getTxnExternalId(); + + LoanTransaction disburseTransaction = this.loanAccountDomainService.makeDisburseTransaction(accountTransferDTO.getFromAccountId(), + accountTransferDTO.getTransactionDate(), accountTransferDTO.getTransactionAmount(), accountTransferDTO.getPaymentDetail(), + accountTransferDTO.getNoteText(), externalIdForDisbursement, true); + final String chargeRefundChargeType = null; + + ExternalId externalIdForRepayment = externalIdFactory.create(); + + LoanTransaction repayTransaction = this.loanAccountDomainService.makeRepayment(LoanTransactionType.REPAYMENT, toLoanAccount, + accountTransferDTO.getTransactionDate(), accountTransferDTO.getTransactionAmount(), accountTransferDTO.getPaymentDetail(), + null, externalIdForRepayment, false, chargeRefundChargeType, isAccountTransfer, null, false, true); + + AccountTransferDetails accountTransferDetails = this.accountTransferAssembler.assembleLoanToLoanTransfer(accountTransferDTO, + fromLoanAccount, toLoanAccount, disburseTransaction, repayTransaction); + this.accountTransferDetailRepository.saveAndFlush(accountTransferDetails); + + return accountTransferDetails; + } + + private boolean isLoanToSavingsAccountTransfer(final PortfolioAccountType fromAccountType, final PortfolioAccountType toAccountType) { + return fromAccountType.isLoanAccount() && toAccountType.isSavingsAccount(); + } + + private boolean isSavingsToLoanAccountTransfer(final PortfolioAccountType fromAccountType, final PortfolioAccountType toAccountType) { + return fromAccountType.isSavingsAccount() && toAccountType.isLoanAccount(); + } + + private boolean isSavingsToSavingsAccountTransfer(final PortfolioAccountType fromAccountType, + final PortfolioAccountType toAccountType) { + return fromAccountType.isSavingsAccount() && toAccountType.isSavingsAccount(); + } + + private boolean isCurrentToSavingsAccountTransfer(final PortfolioAccountType fromAccountType, + final PortfolioAccountType toAccountType) { + return fromAccountType.isCurrentAccount() && toAccountType.isSavingsAccount(); + } + + private boolean isSavingsToCurrentAccountTransfer(final PortfolioAccountType fromAccountType, + final PortfolioAccountType toAccountType) { + return fromAccountType.isSavingsAccount() && toAccountType.isCurrentAccount(); + } + + private boolean isCurrentToCurrentAccountTransfer(final PortfolioAccountType fromAccountType, + final PortfolioAccountType toAccountType) { + return fromAccountType.isCurrentAccount() && toAccountType.isCurrentAccount(); + } + + private boolean isCurrentToLoanAccountTransfer(final PortfolioAccountType fromAccountType, + final PortfolioAccountType toAccountType) { + return fromAccountType.isCurrentAccount() && toAccountType.isLoanAccount(); + } + + /** + * Withdraw amount from a current account and save the transaction. + * Throws {@link GeneralPlatformDomainRuleException} if balance is insufficient. + */ + private CurrentAccountTransaction performCurrentWithdrawal(final CurrentAccount account, final BigDecimal amount, + final LocalDate txnDate) { + final BigDecimal balance = account.getAccountBalanceDerived() != null ? account.getAccountBalanceDerived() : BigDecimal.ZERO; + final BigDecimal onHold = account.getOnHoldFundsDerived() != null ? account.getOnHoldFundsDerived() : BigDecimal.ZERO; + final BigDecimal available = balance.subtract(onHold); + if (!account.isAllowOverdraft() && amount.compareTo(available) > 0) { + throw new GeneralPlatformDomainRuleException("error.msg.current.account.insufficient.balance", + "Insufficient balance in current account " + account.getId() + + ". Available: " + available + ", Requested: " + amount, account.getId()); + } + final CurrentAccountTransaction txn = CurrentAccountTransaction.withdrawal(account, account.getOffice(), amount, txnDate, null); + final BigDecimal newBalance = balance.subtract(amount); + txn.setRunningBalanceDerived(newBalance); + currentAccountTransactionRepository.saveAndFlush(txn); + account.setAccountBalanceDerived(newBalance); + account.setTotalWithdrawalsDerived( + (account.getTotalWithdrawalsDerived() != null ? account.getTotalWithdrawalsDerived() : BigDecimal.ZERO).add(amount)); + currentAccountRepository.save(account); + return txn; + } + + /** + * Deposit amount into a current account and save the transaction. + */ + private CurrentAccountTransaction performCurrentDeposit(final CurrentAccount account, final BigDecimal amount, + final LocalDate txnDate) { + final BigDecimal balance = account.getAccountBalanceDerived() != null ? account.getAccountBalanceDerived() : BigDecimal.ZERO; + final CurrentAccountTransaction txn = CurrentAccountTransaction.deposit(account, account.getOffice(), amount, txnDate, null); + final BigDecimal newBalance = balance.add(amount); + txn.setRunningBalanceDerived(newBalance); + currentAccountTransactionRepository.saveAndFlush(txn); + account.setAccountBalanceDerived(newBalance); + account.setTotalDepositsDerived( + (account.getTotalDepositsDerived() != null ? account.getTotalDepositsDerived() : BigDecimal.ZERO).add(amount)); + currentAccountRepository.save(account); + return txn; + } + + @Override + @Transactional + public CommandProcessingResult refundByTransfer(JsonCommand command) { + // TODO Auto-generated method stub + this.accountTransfersDataValidator.validate(command); + + final LocalDate transactionDate = command.localDateValueOfParameterNamed(transferDateParamName); + final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed(transferAmountParamName); + + final Locale locale = command.extractLocale(); + final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); + + final PaymentDetail paymentDetail = null; + Long transferTransactionId = null; + + final Long fromLoanAccountId = command.longValueOfParameterNamed(fromAccountIdParamName); + final Loan fromLoanAccount = this.loanAccountAssembler.assembleFrom(fromLoanAccountId); + + BigDecimal overpaid = this.loanReadPlatformService.retrieveTotalPaidInAdvance(fromLoanAccountId).getPaidInAdvance(); + final boolean backdatedTxnsAllowedTill = false; + + if (overpaid == null || overpaid.compareTo(BigDecimal.ZERO) == 0 || transactionAmount.floatValue() > overpaid.floatValue()) { + if (overpaid == null) { + overpaid = BigDecimal.ZERO; + } + throw new InvalidPaidInAdvanceAmountException(overpaid.toPlainString()); + } + + ExternalId externalId = externalIdFactory.create(); + + final LoanTransaction loanRefundTransaction = this.loanAccountDomainService.makeRefundForActiveLoan(fromLoanAccountId, + new CommandProcessingResultBuilder(), transactionDate, transactionAmount, paymentDetail, null, externalId); + + final Long toSavingsAccountId = command.longValueOfParameterNamed(toAccountIdParamName); + final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsAccountId, backdatedTxnsAllowedTill); + + final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount, fmt, transactionDate, + transactionAmount, paymentDetail, true, true, backdatedTxnsAllowedTill); + + final AccountTransferDetails accountTransferDetails = this.accountTransferAssembler.assembleLoanToSavingsTransfer(command, + fromLoanAccount, toSavingsAccount, deposit, loanRefundTransaction); + this.accountTransferDetailRepository.saveAndFlush(accountTransferDetails); + transferTransactionId = accountTransferDetails.getId(); + + final CommandProcessingResultBuilder builder = new CommandProcessingResultBuilder().withEntityId(transferTransactionId); + + // if (fromAccountType.isSavingsAccount()) { + + builder.withSavingsId(toSavingsAccountId); + // } + + return builder.build(); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/PortfolioAccountReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/PortfolioAccountReadPlatformServiceImpl.java index 61d5beb0f..0254d4c0c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/PortfolioAccountReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/PortfolioAccountReadPlatformServiceImpl.java @@ -1,396 +1,482 @@ -/** - * 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.account.service; - -import java.math.BigDecimal; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import org.apache.fineract.infrastructure.core.domain.JdbcSupport; -import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; -import org.apache.fineract.organisation.monetary.data.CurrencyData; -import org.apache.fineract.portfolio.account.PortfolioAccountType; -import org.apache.fineract.portfolio.account.data.PortfolioAccountDTO; -import org.apache.fineract.portfolio.account.data.PortfolioAccountData; -import org.apache.fineract.portfolio.account.exception.AccountTransferNotFoundException; -import org.springframework.dao.EmptyResultDataAccessException; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.RowMapper; - -public class PortfolioAccountReadPlatformServiceImpl implements PortfolioAccountReadPlatformService { - - private final JdbcTemplate jdbcTemplate; - - // mapper - private final PortfolioSavingsAccountMapper savingsAccountMapper; - private final PortfolioLoanAccountMapper loanAccountMapper; - private final PortfolioLoanAccountRefundByTransferMapper accountRefundByTransferMapper; - - public PortfolioAccountReadPlatformServiceImpl(final JdbcTemplate jdbcTemplate, DatabaseSpecificSQLGenerator sqlGenerator) { - this.jdbcTemplate = jdbcTemplate; - this.savingsAccountMapper = new PortfolioSavingsAccountMapper(); - this.loanAccountMapper = new PortfolioLoanAccountMapper(); - this.accountRefundByTransferMapper = new PortfolioLoanAccountRefundByTransferMapper(sqlGenerator); - } - - @Override - public PortfolioAccountData retrieveOne(final Long accountId, final Integer accountTypeId) { - return retrieveOne(accountId, accountTypeId, null); - } - - @Override - public PortfolioAccountData retrieveOne(final Long accountId, final Integer accountTypeId, final String currencyCode) { - - Object[] sqlParams = new Object[] { accountId }; - PortfolioAccountData accountData = null; - try { - String sql = null; - final PortfolioAccountType accountType = PortfolioAccountType.fromInt(accountTypeId); - switch (accountType) { - case INVALID: - break; - case LOAN: - - sql = "select " + this.loanAccountMapper.schema() + " where la.id = ?"; - if (currencyCode != null) { - sql += " and la.currency_code = ?"; - sqlParams = new Object[] { accountId, currencyCode }; - } - - accountData = this.jdbcTemplate.queryForObject(sql, this.loanAccountMapper, sqlParams); - break; - case SAVINGS: - sql = "select " + this.savingsAccountMapper.schema() + " where sa.id = ?"; - if (currencyCode != null) { - sql += " and sa.currency_code = ?"; - sqlParams = new Object[] { accountId, currencyCode }; - } - - accountData = this.jdbcTemplate.queryForObject(sql, this.savingsAccountMapper, sqlParams); - break; - } - } catch (final EmptyResultDataAccessException e) { - throw new AccountTransferNotFoundException(accountId, e); - } - - return accountData; - } - - @Override - public Collection retrieveAllForLookup(final PortfolioAccountDTO portfolioAccountDTO) { - - final List sqlParams = new ArrayList<>(); - // sqlParams.add(portfolioAccountDTO.getClientId()); - Collection accounts = null; - String sql = null; - long defaultAccountStatus = 300; // Active Status - if (portfolioAccountDTO.getAccountStatus() != null) { - defaultAccountStatus = portfolioAccountDTO.getFirstAccountStatus(); - } - final PortfolioAccountType accountType = PortfolioAccountType.fromInt(portfolioAccountDTO.getAccountTypeId()); - switch (accountType) { - case INVALID: - break; - case LOAN: - sql = "select " + this.loanAccountMapper.schema() + " where "; - if (portfolioAccountDTO.getClientId() != null) { - sql += " la.client_id = ? and la.loan_status_id in (?) "; - sqlParams.add(portfolioAccountDTO.getClientId()); - sqlParams.add(defaultAccountStatus); - } else { - sql += " la.loan_status_id in (?) "; - sqlParams.add(defaultAccountStatus); - } - if (portfolioAccountDTO.getCurrencyCode() != null) { - sql += " and la.currency_code = ?"; - sqlParams.add(portfolioAccountDTO.getCurrencyCode()); - } - - accounts = this.jdbcTemplate.query(sql, this.loanAccountMapper, sqlParams.toArray()); // NOSONAR - break; - case SAVINGS: - sql = "select " + this.savingsAccountMapper.schema() + " where "; - if (portfolioAccountDTO.getClientId() != null) { - sql += " sa.client_id = ? and sa.status_enum in (?) "; - sqlParams.add(portfolioAccountDTO.getClientId()); - sqlParams.add(defaultAccountStatus); - } else { - sql += " sa.status_enum in (?) "; - sqlParams.add(defaultAccountStatus); - } - if (portfolioAccountDTO.getCurrencyCode() != null) { - sql += " and sa.currency_code = ?"; - sqlParams.add(portfolioAccountDTO.getCurrencyCode()); - } - - if (portfolioAccountDTO.getDepositType() != null) { - sql += " and sa.deposit_type_enum = ?"; - sqlParams.add(portfolioAccountDTO.getDepositType().shortValue()); - } - - if (portfolioAccountDTO.isExcludeOverDraftAccounts()) { - sql += " and sa.allow_overdraft = false"; - } - - if (portfolioAccountDTO.getClientId() == null && portfolioAccountDTO.getGroupId() != null) { - sql += " and sa.group_id = ? "; - sqlParams.add(portfolioAccountDTO.getGroupId()); - } - - accounts = this.jdbcTemplate.query(sql, this.savingsAccountMapper, sqlParams.toArray()); // NOSONAR - break; - } - - return accounts; - } - - private static final class PortfolioSavingsAccountMapper implements RowMapper { - - private final String schemaSql; - - PortfolioSavingsAccountMapper() { - - final StringBuilder sqlBuilder = new StringBuilder(400); - sqlBuilder.append("sa.id as id, sa.account_no as accountNo, sa.external_id as externalId, "); - sqlBuilder.append("c.id as clientId, c.display_name as clientName, "); - sqlBuilder.append("g.id as groupId, g.display_name as groupName, "); - sqlBuilder.append("sp.id as productId, sp.name as productName, "); - sqlBuilder.append("s.id as fieldOfficerId, s.display_name as fieldOfficerName, "); - sqlBuilder.append("sa.currency_code as currencyCode, sa.currency_digits as currencyDigits,"); - sqlBuilder.append("sa.currency_multiplesof as inMultiplesOf, "); - sqlBuilder.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, "); - sqlBuilder.append("curr.display_symbol as currencyDisplaySymbol "); - sqlBuilder.append("from m_savings_account sa "); - sqlBuilder.append("join m_savings_product sp ON sa.product_id = sp.id "); - sqlBuilder.append("join m_currency curr on curr.code = sa.currency_code "); - sqlBuilder.append("left join m_client c ON c.id = sa.client_id "); - sqlBuilder.append("left join m_group g ON g.id = sa.group_id "); - sqlBuilder.append("left join m_staff s ON s.id = sa.field_officer_id "); - - this.schemaSql = sqlBuilder.toString(); - } - - public String schema() { - return this.schemaSql; - } - - @Override - public PortfolioAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { - - final Long id = rs.getLong("id"); - final String accountNo = rs.getString("accountNo"); - final String externalId = rs.getString("externalId"); - - final Long groupId = JdbcSupport.getLong(rs, "groupId"); - final String groupName = rs.getString("groupName"); - final Long clientId = JdbcSupport.getLong(rs, "clientId"); - final String clientName = rs.getString("clientName"); - - final Long productId = rs.getLong("productId"); - final String productName = rs.getString("productName"); - - final Long fieldOfficerId = rs.getLong("fieldOfficerId"); - final String fieldOfficerName = rs.getString("fieldOfficerName"); - - final String currencyCode = rs.getString("currencyCode"); - final String currencyName = rs.getString("currencyName"); - final String currencyNameCode = rs.getString("currencyNameCode"); - final String currencyDisplaySymbol = rs.getString("currencyDisplaySymbol"); - final Integer currencyDigits = JdbcSupport.getInteger(rs, "currencyDigits"); - final Integer inMulitplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf"); - final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMulitplesOf, currencyDisplaySymbol, - currencyNameCode); - - return new PortfolioAccountData(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName, - fieldOfficerId, fieldOfficerName, currency); - } - } - - private static final class PortfolioLoanAccountMapper implements RowMapper { - - private final String schemaSql; - - PortfolioLoanAccountMapper() { - - final StringBuilder sqlBuilder = new StringBuilder(400); - sqlBuilder.append("la.id as id, la.account_no as accountNo, la.external_id as externalId, "); - sqlBuilder.append("c.id as clientId, c.display_name as clientName, "); - sqlBuilder.append("g.id as groupId, g.display_name as groupName, "); - sqlBuilder.append("lp.id as productId, lp.name as productName, "); - sqlBuilder.append("s.id as fieldOfficerId, s.display_name as fieldOfficerName, "); - sqlBuilder.append("la.currency_code as currencyCode, la.currency_digits as currencyDigits,"); - sqlBuilder.append("la.currency_multiplesof as inMultiplesOf, "); - sqlBuilder.append("la.total_overpaid_derived as totalOverpaid, "); - sqlBuilder.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, "); - sqlBuilder.append("curr.display_symbol as currencyDisplaySymbol "); - sqlBuilder.append("from m_loan la "); - sqlBuilder.append("join m_product_loan lp ON la.product_id = lp.id "); - sqlBuilder.append("join m_currency curr on curr.code = la.currency_code "); - sqlBuilder.append("left join m_client c ON c.id = la.client_id "); - sqlBuilder.append("left join m_group g ON g.id = la.group_id "); - sqlBuilder.append("left join m_staff s ON s.id = la.loan_officer_id "); - - this.schemaSql = sqlBuilder.toString(); - } - - public String schema() { - return this.schemaSql; - } - - @Override - public PortfolioAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { - - final Long id = rs.getLong("id"); - final String accountNo = rs.getString("accountNo"); - final String externalId = rs.getString("externalId"); - - final Long groupId = JdbcSupport.getLong(rs, "groupId"); - final String groupName = rs.getString("groupName"); - final Long clientId = JdbcSupport.getLong(rs, "clientId"); - final String clientName = rs.getString("clientName"); - - final Long productId = rs.getLong("productId"); - final String productName = rs.getString("productName"); - - final Long fieldOfficerId = rs.getLong("fieldOfficerId"); - final String fieldOfficerName = rs.getString("fieldOfficerName"); - - final String currencyCode = rs.getString("currencyCode"); - final String currencyName = rs.getString("currencyName"); - final String currencyNameCode = rs.getString("currencyNameCode"); - final String currencyDisplaySymbol = rs.getString("currencyDisplaySymbol"); - final Integer currencyDigits = JdbcSupport.getInteger(rs, "currencyDigits"); - final Integer inMulitplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf"); - final BigDecimal amtForTransfer = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalOverpaid"); - final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMulitplesOf, currencyDisplaySymbol, - currencyNameCode); - - return new PortfolioAccountData(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName, - fieldOfficerId, fieldOfficerName, currency, amtForTransfer); - } - } - - private static final class PortfolioLoanAccountRefundByTransferMapper implements RowMapper { - - private String schemaSql; - private final DatabaseSpecificSQLGenerator sqlGenerator; - - PortfolioLoanAccountRefundByTransferMapper(DatabaseSpecificSQLGenerator sqlGenerator) { - this.sqlGenerator = sqlGenerator; - } - - public String schema() { - final StringBuilder amountQueryString = new StringBuilder(400); - amountQueryString.append("(select (SUM(COALESCE(mr.principal_completed_derived, 0)) +"); - amountQueryString.append("SUM(COALESCE(mr.interest_completed_derived, 0)) + "); - amountQueryString.append("SUM(COALESCE(mr.fee_charges_completed_derived, 0)) + "); - amountQueryString.append(" SUM(COALESCE(mr.penalty_charges_completed_derived, 0))) as total_in_advance_derived"); - amountQueryString.append(" from m_loan ml INNER JOIN m_loan_repayment_schedule mr on mr.loan_id = ml.id"); - amountQueryString.append(" where ml.id=? and ml.loan_status_id = 300"); - amountQueryString.append(" and mr.duedate >= " + sqlGenerator.currentBusinessDate() + " group by ml.id having"); - amountQueryString.append(" (SUM(COALESCE(mr.principal_completed_derived, 0)) + "); - amountQueryString.append(" SUM(COALESCE(mr.interest_completed_derived, 0)) + "); - amountQueryString.append("SUM(COALESCE(mr.fee_charges_completed_derived, 0)) + "); - amountQueryString.append("SUM(COALESCE(mr.penalty_charges_completed_derived, 0))) > 0) as totalOverpaid "); - - final StringBuilder sqlBuilder = new StringBuilder(400); - sqlBuilder.append("la.id as id, la.account_no as accountNo, la.external_id as externalId, "); - sqlBuilder.append("c.id as clientId, c.display_name as clientName, "); - sqlBuilder.append("g.id as groupId, g.display_name as groupName, "); - sqlBuilder.append("lp.id as productId, lp.name as productName, "); - sqlBuilder.append("s.id as fieldOfficerId, s.display_name as fieldOfficerName, "); - sqlBuilder.append("la.currency_code as currencyCode, la.currency_digits as currencyDigits,"); - sqlBuilder.append("la.currency_multiplesof as inMultiplesOf, "); - sqlBuilder.append(amountQueryString.toString()); - sqlBuilder.append(", "); - sqlBuilder.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, "); - sqlBuilder.append("curr.display_symbol as currencyDisplaySymbol "); - sqlBuilder.append("from m_loan la "); - sqlBuilder.append("join m_product_loan lp ON la.product_id = lp.id "); - sqlBuilder.append("join m_currency curr on curr.code = la.currency_code "); - sqlBuilder.append("left join m_client c ON c.id = la.client_id "); - sqlBuilder.append("left join m_group g ON g.id = la.group_id "); - sqlBuilder.append("left join m_staff s ON s.id = la.loan_officer_id "); - - this.schemaSql = sqlBuilder.toString(); - - return this.schemaSql; - } - - @Override - public PortfolioAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { - - final Long id = rs.getLong("id"); - final String accountNo = rs.getString("accountNo"); - final String externalId = rs.getString("externalId"); - - final Long groupId = JdbcSupport.getLong(rs, "groupId"); - final String groupName = rs.getString("groupName"); - final Long clientId = JdbcSupport.getLong(rs, "clientId"); - final String clientName = rs.getString("clientName"); - - final Long productId = rs.getLong("productId"); - final String productName = rs.getString("productName"); - - final Long fieldOfficerId = rs.getLong("fieldOfficerId"); - final String fieldOfficerName = rs.getString("fieldOfficerName"); - - final String currencyCode = rs.getString("currencyCode"); - final String currencyName = rs.getString("currencyName"); - final String currencyNameCode = rs.getString("currencyNameCode"); - final String currencyDisplaySymbol = rs.getString("currencyDisplaySymbol"); - final Integer currencyDigits = JdbcSupport.getInteger(rs, "currencyDigits"); - final Integer inMulitplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf"); - final BigDecimal amtForTransfer = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalOverpaid"); - final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMulitplesOf, currencyDisplaySymbol, - currencyNameCode); - - return new PortfolioAccountData(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName, - fieldOfficerId, fieldOfficerName, currency, amtForTransfer); - } - } - - @Override - public PortfolioAccountData retrieveOneByPaidInAdvance(Long accountId, Integer accountTypeId) { - // TODO Auto-generated method stub - Object[] sqlParams = new Object[] { accountId, accountId }; - PortfolioAccountData accountData = null; - // String currencyCode = null; - try { - String sql = null; - // final PortfolioAccountType accountType = - // PortfolioAccountType.fromInt(accountTypeId); - - sql = "select " + this.accountRefundByTransferMapper.schema() + " where la.id = ?"; - /* - * if (currencyCode != null) { sql += " and la.currency_code = ?"; sqlParams = new Object[] {accountId , - * accountId,currencyCode }; } - */ - - accountData = this.jdbcTemplate.queryForObject(sql, this.accountRefundByTransferMapper, sqlParams); - - } catch (final EmptyResultDataAccessException e) { - throw new AccountTransferNotFoundException(accountId, e); - } - - return accountData; - } -} +/** + * 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.account.service; + +import java.math.BigDecimal; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import org.apache.fineract.infrastructure.core.domain.JdbcSupport; +import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; +import org.apache.fineract.organisation.monetary.data.CurrencyData; +import org.apache.fineract.portfolio.account.PortfolioAccountType; +import org.apache.fineract.portfolio.account.data.PortfolioAccountDTO; +import org.apache.fineract.portfolio.account.data.PortfolioAccountData; +import org.apache.fineract.portfolio.account.exception.AccountTransferNotFoundException; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; + +public class PortfolioAccountReadPlatformServiceImpl implements PortfolioAccountReadPlatformService { + + private final JdbcTemplate jdbcTemplate; + + // mapper + private final PortfolioSavingsAccountMapper savingsAccountMapper; + private final PortfolioLoanAccountMapper loanAccountMapper; + private final PortfolioLoanAccountRefundByTransferMapper accountRefundByTransferMapper; + private final PortfolioCurrentAccountMapper currentAccountMapper; + + public PortfolioAccountReadPlatformServiceImpl(final JdbcTemplate jdbcTemplate, DatabaseSpecificSQLGenerator sqlGenerator) { + this.jdbcTemplate = jdbcTemplate; + this.savingsAccountMapper = new PortfolioSavingsAccountMapper(); + this.loanAccountMapper = new PortfolioLoanAccountMapper(); + this.accountRefundByTransferMapper = new PortfolioLoanAccountRefundByTransferMapper(sqlGenerator); + this.currentAccountMapper = new PortfolioCurrentAccountMapper(); + } + + @Override + public PortfolioAccountData retrieveOne(final Long accountId, final Integer accountTypeId) { + return retrieveOne(accountId, accountTypeId, null); + } + + @Override + public PortfolioAccountData retrieveOne(final Long accountId, final Integer accountTypeId, final String currencyCode) { + + Object[] sqlParams = new Object[] { accountId }; + PortfolioAccountData accountData = null; + try { + String sql = null; + final PortfolioAccountType accountType = PortfolioAccountType.fromInt(accountTypeId); + switch (accountType) { + case INVALID: + break; + case LOAN: + + sql = "select " + this.loanAccountMapper.schema() + " where la.id = ?"; + if (currencyCode != null) { + sql += " and la.currency_code = ?"; + sqlParams = new Object[] { accountId, currencyCode }; + } + + accountData = this.jdbcTemplate.queryForObject(sql, this.loanAccountMapper, sqlParams); + break; + case SAVINGS: + sql = "select " + this.savingsAccountMapper.schema() + " where sa.id = ?"; + if (currencyCode != null) { + sql += " and sa.currency_code = ?"; + sqlParams = new Object[] { accountId, currencyCode }; + } + + accountData = this.jdbcTemplate.queryForObject(sql, this.savingsAccountMapper, sqlParams); + break; + case CURRENT: + sql = "select " + this.currentAccountMapper.schema() + " where ca.id = ?"; + if (currencyCode != null) { + sql += " and ca.currency_code = ?"; + sqlParams = new Object[] { accountId, currencyCode }; + } + + accountData = this.jdbcTemplate.queryForObject(sql, this.currentAccountMapper, sqlParams); + break; + } + } catch (final EmptyResultDataAccessException e) { + throw new AccountTransferNotFoundException(accountId, e); + } + + return accountData; + } + + @Override + public Collection retrieveAllForLookup(final PortfolioAccountDTO portfolioAccountDTO) { + + final List sqlParams = new ArrayList<>(); + // sqlParams.add(portfolioAccountDTO.getClientId()); + Collection accounts = null; + String sql = null; + long defaultAccountStatus = 300; // Active Status + if (portfolioAccountDTO.getAccountStatus() != null) { + defaultAccountStatus = portfolioAccountDTO.getFirstAccountStatus(); + } + final PortfolioAccountType accountType = PortfolioAccountType.fromInt(portfolioAccountDTO.getAccountTypeId()); + switch (accountType) { + case INVALID: + break; + case LOAN: + sql = "select " + this.loanAccountMapper.schema() + " where "; + if (portfolioAccountDTO.getClientId() != null) { + sql += " la.client_id = ? and la.loan_status_id in (?) "; + sqlParams.add(portfolioAccountDTO.getClientId()); + sqlParams.add(defaultAccountStatus); + } else { + sql += " la.loan_status_id in (?) "; + sqlParams.add(defaultAccountStatus); + } + if (portfolioAccountDTO.getCurrencyCode() != null) { + sql += " and la.currency_code = ?"; + sqlParams.add(portfolioAccountDTO.getCurrencyCode()); + } + + accounts = this.jdbcTemplate.query(sql, this.loanAccountMapper, sqlParams.toArray()); // NOSONAR + break; + case SAVINGS: + sql = "select " + this.savingsAccountMapper.schema() + " where "; + if (portfolioAccountDTO.getClientId() != null) { + sql += " sa.client_id = ? and sa.status_enum in (?) "; + sqlParams.add(portfolioAccountDTO.getClientId()); + sqlParams.add(defaultAccountStatus); + } else { + sql += " sa.status_enum in (?) "; + sqlParams.add(defaultAccountStatus); + } + if (portfolioAccountDTO.getCurrencyCode() != null) { + sql += " and sa.currency_code = ?"; + sqlParams.add(portfolioAccountDTO.getCurrencyCode()); + } + + if (portfolioAccountDTO.getDepositType() != null) { + sql += " and sa.deposit_type_enum = ?"; + sqlParams.add(portfolioAccountDTO.getDepositType().shortValue()); + } + + if (portfolioAccountDTO.isExcludeOverDraftAccounts()) { + sql += " and sa.allow_overdraft = false"; + } + + if (portfolioAccountDTO.getClientId() == null && portfolioAccountDTO.getGroupId() != null) { + sql += " and sa.group_id = ? "; + sqlParams.add(portfolioAccountDTO.getGroupId()); + } + + accounts = this.jdbcTemplate.query(sql, this.savingsAccountMapper, sqlParams.toArray()); // NOSONAR + break; + case CURRENT: + sql = "select " + this.currentAccountMapper.schema() + " where "; + if (portfolioAccountDTO.getClientId() != null) { + sql += " ca.client_id = ? and ca.status_enum in (?) "; + sqlParams.add(portfolioAccountDTO.getClientId()); + sqlParams.add(defaultAccountStatus); + } else { + sql += " ca.status_enum in (?) "; + sqlParams.add(defaultAccountStatus); + } + if (portfolioAccountDTO.getCurrencyCode() != null) { + sql += " and ca.currency_code = ?"; + sqlParams.add(portfolioAccountDTO.getCurrencyCode()); + } + + accounts = this.jdbcTemplate.query(sql, this.currentAccountMapper, sqlParams.toArray()); // NOSONAR + break; + } + + return accounts; + } + + private static final class PortfolioSavingsAccountMapper implements RowMapper { + + private final String schemaSql; + + PortfolioSavingsAccountMapper() { + + final StringBuilder sqlBuilder = new StringBuilder(400); + sqlBuilder.append("sa.id as id, sa.account_no as accountNo, sa.external_id as externalId, "); + sqlBuilder.append("c.id as clientId, c.display_name as clientName, "); + sqlBuilder.append("g.id as groupId, g.display_name as groupName, "); + sqlBuilder.append("sp.id as productId, sp.name as productName, "); + sqlBuilder.append("s.id as fieldOfficerId, s.display_name as fieldOfficerName, "); + sqlBuilder.append("sa.currency_code as currencyCode, sa.currency_digits as currencyDigits,"); + sqlBuilder.append("sa.currency_multiplesof as inMultiplesOf, "); + sqlBuilder.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, "); + sqlBuilder.append("curr.display_symbol as currencyDisplaySymbol "); + sqlBuilder.append("from m_savings_account sa "); + sqlBuilder.append("join m_savings_product sp ON sa.product_id = sp.id "); + sqlBuilder.append("join m_currency curr on curr.code = sa.currency_code "); + sqlBuilder.append("left join m_client c ON c.id = sa.client_id "); + sqlBuilder.append("left join m_group g ON g.id = sa.group_id "); + sqlBuilder.append("left join m_staff s ON s.id = sa.field_officer_id "); + + this.schemaSql = sqlBuilder.toString(); + } + + public String schema() { + return this.schemaSql; + } + + @Override + public PortfolioAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { + + final Long id = rs.getLong("id"); + final String accountNo = rs.getString("accountNo"); + final String externalId = rs.getString("externalId"); + + final Long groupId = JdbcSupport.getLong(rs, "groupId"); + final String groupName = rs.getString("groupName"); + final Long clientId = JdbcSupport.getLong(rs, "clientId"); + final String clientName = rs.getString("clientName"); + + final Long productId = rs.getLong("productId"); + final String productName = rs.getString("productName"); + + final Long fieldOfficerId = rs.getLong("fieldOfficerId"); + final String fieldOfficerName = rs.getString("fieldOfficerName"); + + final String currencyCode = rs.getString("currencyCode"); + final String currencyName = rs.getString("currencyName"); + final String currencyNameCode = rs.getString("currencyNameCode"); + final String currencyDisplaySymbol = rs.getString("currencyDisplaySymbol"); + final Integer currencyDigits = JdbcSupport.getInteger(rs, "currencyDigits"); + final Integer inMulitplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf"); + final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMulitplesOf, currencyDisplaySymbol, + currencyNameCode); + + return new PortfolioAccountData(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName, + fieldOfficerId, fieldOfficerName, currency); + } + } + + private static final class PortfolioLoanAccountMapper implements RowMapper { + + private final String schemaSql; + + PortfolioLoanAccountMapper() { + + final StringBuilder sqlBuilder = new StringBuilder(400); + sqlBuilder.append("la.id as id, la.account_no as accountNo, la.external_id as externalId, "); + sqlBuilder.append("c.id as clientId, c.display_name as clientName, "); + sqlBuilder.append("g.id as groupId, g.display_name as groupName, "); + sqlBuilder.append("lp.id as productId, lp.name as productName, "); + sqlBuilder.append("s.id as fieldOfficerId, s.display_name as fieldOfficerName, "); + sqlBuilder.append("la.currency_code as currencyCode, la.currency_digits as currencyDigits,"); + sqlBuilder.append("la.currency_multiplesof as inMultiplesOf, "); + sqlBuilder.append("la.total_overpaid_derived as totalOverpaid, "); + sqlBuilder.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, "); + sqlBuilder.append("curr.display_symbol as currencyDisplaySymbol "); + sqlBuilder.append("from m_loan la "); + sqlBuilder.append("join m_product_loan lp ON la.product_id = lp.id "); + sqlBuilder.append("join m_currency curr on curr.code = la.currency_code "); + sqlBuilder.append("left join m_client c ON c.id = la.client_id "); + sqlBuilder.append("left join m_group g ON g.id = la.group_id "); + sqlBuilder.append("left join m_staff s ON s.id = la.loan_officer_id "); + + this.schemaSql = sqlBuilder.toString(); + } + + public String schema() { + return this.schemaSql; + } + + @Override + public PortfolioAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { + + final Long id = rs.getLong("id"); + final String accountNo = rs.getString("accountNo"); + final String externalId = rs.getString("externalId"); + + final Long groupId = JdbcSupport.getLong(rs, "groupId"); + final String groupName = rs.getString("groupName"); + final Long clientId = JdbcSupport.getLong(rs, "clientId"); + final String clientName = rs.getString("clientName"); + + final Long productId = rs.getLong("productId"); + final String productName = rs.getString("productName"); + + final Long fieldOfficerId = rs.getLong("fieldOfficerId"); + final String fieldOfficerName = rs.getString("fieldOfficerName"); + + final String currencyCode = rs.getString("currencyCode"); + final String currencyName = rs.getString("currencyName"); + final String currencyNameCode = rs.getString("currencyNameCode"); + final String currencyDisplaySymbol = rs.getString("currencyDisplaySymbol"); + final Integer currencyDigits = JdbcSupport.getInteger(rs, "currencyDigits"); + final Integer inMulitplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf"); + final BigDecimal amtForTransfer = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalOverpaid"); + final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMulitplesOf, currencyDisplaySymbol, + currencyNameCode); + + return new PortfolioAccountData(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName, + fieldOfficerId, fieldOfficerName, currency, amtForTransfer); + } + } + + private static final class PortfolioLoanAccountRefundByTransferMapper implements RowMapper { + + private String schemaSql; + private final DatabaseSpecificSQLGenerator sqlGenerator; + + PortfolioLoanAccountRefundByTransferMapper(DatabaseSpecificSQLGenerator sqlGenerator) { + this.sqlGenerator = sqlGenerator; + } + + public String schema() { + final StringBuilder amountQueryString = new StringBuilder(400); + amountQueryString.append("(select (SUM(COALESCE(mr.principal_completed_derived, 0)) +"); + amountQueryString.append("SUM(COALESCE(mr.interest_completed_derived, 0)) + "); + amountQueryString.append("SUM(COALESCE(mr.fee_charges_completed_derived, 0)) + "); + amountQueryString.append(" SUM(COALESCE(mr.penalty_charges_completed_derived, 0))) as total_in_advance_derived"); + amountQueryString.append(" from m_loan ml INNER JOIN m_loan_repayment_schedule mr on mr.loan_id = ml.id"); + amountQueryString.append(" where ml.id=? and ml.loan_status_id = 300"); + amountQueryString.append(" and mr.duedate >= " + sqlGenerator.currentBusinessDate() + " group by ml.id having"); + amountQueryString.append(" (SUM(COALESCE(mr.principal_completed_derived, 0)) + "); + amountQueryString.append(" SUM(COALESCE(mr.interest_completed_derived, 0)) + "); + amountQueryString.append("SUM(COALESCE(mr.fee_charges_completed_derived, 0)) + "); + amountQueryString.append("SUM(COALESCE(mr.penalty_charges_completed_derived, 0))) > 0) as totalOverpaid "); + + final StringBuilder sqlBuilder = new StringBuilder(400); + sqlBuilder.append("la.id as id, la.account_no as accountNo, la.external_id as externalId, "); + sqlBuilder.append("c.id as clientId, c.display_name as clientName, "); + sqlBuilder.append("g.id as groupId, g.display_name as groupName, "); + sqlBuilder.append("lp.id as productId, lp.name as productName, "); + sqlBuilder.append("s.id as fieldOfficerId, s.display_name as fieldOfficerName, "); + sqlBuilder.append("la.currency_code as currencyCode, la.currency_digits as currencyDigits,"); + sqlBuilder.append("la.currency_multiplesof as inMultiplesOf, "); + sqlBuilder.append(amountQueryString.toString()); + sqlBuilder.append(", "); + sqlBuilder.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, "); + sqlBuilder.append("curr.display_symbol as currencyDisplaySymbol "); + sqlBuilder.append("from m_loan la "); + sqlBuilder.append("join m_product_loan lp ON la.product_id = lp.id "); + sqlBuilder.append("join m_currency curr on curr.code = la.currency_code "); + sqlBuilder.append("left join m_client c ON c.id = la.client_id "); + sqlBuilder.append("left join m_group g ON g.id = la.group_id "); + sqlBuilder.append("left join m_staff s ON s.id = la.loan_officer_id "); + + this.schemaSql = sqlBuilder.toString(); + + return this.schemaSql; + } + + @Override + public PortfolioAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { + + final Long id = rs.getLong("id"); + final String accountNo = rs.getString("accountNo"); + final String externalId = rs.getString("externalId"); + + final Long groupId = JdbcSupport.getLong(rs, "groupId"); + final String groupName = rs.getString("groupName"); + final Long clientId = JdbcSupport.getLong(rs, "clientId"); + final String clientName = rs.getString("clientName"); + + final Long productId = rs.getLong("productId"); + final String productName = rs.getString("productName"); + + final Long fieldOfficerId = rs.getLong("fieldOfficerId"); + final String fieldOfficerName = rs.getString("fieldOfficerName"); + + final String currencyCode = rs.getString("currencyCode"); + final String currencyName = rs.getString("currencyName"); + final String currencyNameCode = rs.getString("currencyNameCode"); + final String currencyDisplaySymbol = rs.getString("currencyDisplaySymbol"); + final Integer currencyDigits = JdbcSupport.getInteger(rs, "currencyDigits"); + final Integer inMulitplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf"); + final BigDecimal amtForTransfer = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalOverpaid"); + final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMulitplesOf, currencyDisplaySymbol, + currencyNameCode); + + return new PortfolioAccountData(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName, + fieldOfficerId, fieldOfficerName, currency, amtForTransfer); + } + } + + @Override + public PortfolioAccountData retrieveOneByPaidInAdvance(Long accountId, Integer accountTypeId) { + // TODO Auto-generated method stub + Object[] sqlParams = new Object[] { accountId, accountId }; + PortfolioAccountData accountData = null; + // String currencyCode = null; + try { + String sql = null; + // final PortfolioAccountType accountType = + // PortfolioAccountType.fromInt(accountTypeId); + + sql = "select " + this.accountRefundByTransferMapper.schema() + " where la.id = ?"; + /* + * if (currencyCode != null) { sql += " and la.currency_code = ?"; sqlParams = new Object[] {accountId , + * accountId,currencyCode }; } + */ + + accountData = this.jdbcTemplate.queryForObject(sql, this.accountRefundByTransferMapper, sqlParams); + + } catch (final EmptyResultDataAccessException e) { + throw new AccountTransferNotFoundException(accountId, e); + } + + return accountData; + } + + private static final class PortfolioCurrentAccountMapper implements RowMapper { + + private final String schemaSql; + + PortfolioCurrentAccountMapper() { + final StringBuilder sqlBuilder = new StringBuilder(400); + sqlBuilder.append("ca.id as id, ca.account_no as accountNo, ca.external_id as externalId, "); + sqlBuilder.append("c.id as clientId, c.display_name as clientName, "); + sqlBuilder.append("null as groupId, null as groupName, "); + sqlBuilder.append("cp.id as productId, cp.name as productName, "); + sqlBuilder.append("null as fieldOfficerId, null as fieldOfficerName, "); + sqlBuilder.append("ca.currency_code as currencyCode, ca.currency_digits as currencyDigits, "); + sqlBuilder.append("ca.currency_multiplesof as inMultiplesOf, "); + sqlBuilder.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, "); + sqlBuilder.append("curr.display_symbol as currencyDisplaySymbol "); + sqlBuilder.append("from m_current_account ca "); + sqlBuilder.append("join m_current_product cp ON ca.product_id = cp.id "); + sqlBuilder.append("join m_currency curr on curr.code = ca.currency_code "); + sqlBuilder.append("left join m_client c ON c.id = ca.client_id "); + + this.schemaSql = sqlBuilder.toString(); + } + + public String schema() { + return this.schemaSql; + } + + @Override + public PortfolioAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { + final Long id = rs.getLong("id"); + final String accountNo = rs.getString("accountNo"); + final String externalId = rs.getString("externalId"); + + final Long groupId = JdbcSupport.getLong(rs, "groupId"); + final String groupName = rs.getString("groupName"); + final Long clientId = JdbcSupport.getLong(rs, "clientId"); + final String clientName = rs.getString("clientName"); + + final Long productId = rs.getLong("productId"); + final String productName = rs.getString("productName"); + + final Long fieldOfficerId = JdbcSupport.getLong(rs, "fieldOfficerId"); + final String fieldOfficerName = rs.getString("fieldOfficerName"); + + final String currencyCode = rs.getString("currencyCode"); + final String currencyName = rs.getString("currencyName"); + final String currencyNameCode = rs.getString("currencyNameCode"); + final String currencyDisplaySymbol = rs.getString("currencyDisplaySymbol"); + final Integer currencyDigits = JdbcSupport.getInteger(rs, "currencyDigits"); + final Integer inMultiplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf"); + final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMultiplesOf, + currencyDisplaySymbol, currencyNameCode); + + return new PortfolioAccountData(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName, + fieldOfficerId, fieldOfficerName, currency); + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/StandingInstructionReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/StandingInstructionReadPlatformServiceImpl.java index 7e86160e2..27b2b2627 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/StandingInstructionReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/StandingInstructionReadPlatformServiceImpl.java @@ -1,591 +1,625 @@ -/** - * 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.account.service; - -import static org.apache.fineract.portfolio.account.service.AccountTransferEnumerations.accountType; -import static org.apache.fineract.portfolio.account.service.AccountTransferEnumerations.recurrenceType; -import static org.apache.fineract.portfolio.account.service.AccountTransferEnumerations.standingInstructionPriority; -import static org.apache.fineract.portfolio.account.service.AccountTransferEnumerations.standingInstructionStatus; -import static org.apache.fineract.portfolio.account.service.AccountTransferEnumerations.standingInstructionType; -import static org.apache.fineract.portfolio.account.service.AccountTransferEnumerations.transferType; - -import java.math.BigDecimal; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.time.LocalDate; -import java.time.MonthDay; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import org.apache.fineract.infrastructure.core.data.EnumOptionData; -import org.apache.fineract.infrastructure.core.domain.JdbcSupport; -import org.apache.fineract.infrastructure.core.service.DateUtils; -import org.apache.fineract.infrastructure.core.service.Page; -import org.apache.fineract.infrastructure.core.service.PaginationHelper; -import org.apache.fineract.infrastructure.core.service.SearchParameters; -import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; -import org.apache.fineract.infrastructure.security.utils.ColumnValidator; -import org.apache.fineract.organisation.office.data.OfficeData; -import org.apache.fineract.organisation.office.service.OfficeReadPlatformService; -import org.apache.fineract.portfolio.account.PortfolioAccountType; -import org.apache.fineract.portfolio.account.data.PortfolioAccountDTO; -import org.apache.fineract.portfolio.account.data.PortfolioAccountData; -import org.apache.fineract.portfolio.account.data.StandingInstructionDTO; -import org.apache.fineract.portfolio.account.data.StandingInstructionData; -import org.apache.fineract.portfolio.account.data.StandingInstructionDuesData; -import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType; -import org.apache.fineract.portfolio.account.domain.AccountTransferType; -import org.apache.fineract.portfolio.account.domain.StandingInstructionPriority; -import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus; -import org.apache.fineract.portfolio.account.domain.StandingInstructionType; -import org.apache.fineract.portfolio.account.exception.AccountTransferNotFoundException; -import org.apache.fineract.portfolio.client.data.ClientData; -import org.apache.fineract.portfolio.client.service.ClientReadPlatformService; -import org.apache.fineract.portfolio.common.service.CommonEnumerations; -import org.apache.fineract.portfolio.common.service.DropdownReadPlatformService; -import org.springframework.dao.EmptyResultDataAccessException; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.RowMapper; -import org.springframework.util.CollectionUtils; - -public class StandingInstructionReadPlatformServiceImpl implements StandingInstructionReadPlatformService { - - private final JdbcTemplate jdbcTemplate; - private final ColumnValidator columnValidator; - private final ClientReadPlatformService clientReadPlatformService; - private final OfficeReadPlatformService officeReadPlatformService; - private final PortfolioAccountReadPlatformService portfolioAccountReadPlatformService; - private final DropdownReadPlatformService dropdownReadPlatformService; - private final DatabaseSpecificSQLGenerator sqlGenerator; - - // mapper - private final StandingInstructionMapper standingInstructionMapper; - - // pagination - private final PaginationHelper paginationHelper; - - public StandingInstructionReadPlatformServiceImpl(final JdbcTemplate jdbcTemplate, - final ClientReadPlatformService clientReadPlatformService, final OfficeReadPlatformService officeReadPlatformService, - final PortfolioAccountReadPlatformService portfolioAccountReadPlatformService, - final DropdownReadPlatformService dropdownReadPlatformService, final ColumnValidator columnValidator, - DatabaseSpecificSQLGenerator sqlGenerator, PaginationHelper paginationHelper) { - this.jdbcTemplate = jdbcTemplate; - this.clientReadPlatformService = clientReadPlatformService; - this.officeReadPlatformService = officeReadPlatformService; - this.portfolioAccountReadPlatformService = portfolioAccountReadPlatformService; - this.dropdownReadPlatformService = dropdownReadPlatformService; - this.sqlGenerator = sqlGenerator; - this.standingInstructionMapper = new StandingInstructionMapper(); - this.columnValidator = columnValidator; - this.paginationHelper = paginationHelper; - } - - @Override - public StandingInstructionData retrieveTemplate(final Long fromOfficeId, final Long fromClientId, final Long fromAccountId, - final Integer fromAccountType, final Long toOfficeId, final Long toClientId, final Long toAccountId, - final Integer toAccountType, Integer transferType) { - - AccountTransferType accountTransferType = AccountTransferType.INVALID; - if (transferType != null) { - accountTransferType = AccountTransferType.fromInt(transferType); - } - - final EnumOptionData loanAccountType = accountType(PortfolioAccountType.LOAN); - final EnumOptionData savingsAccountType = accountType(PortfolioAccountType.SAVINGS); - - final Integer mostRelevantFromAccountType = fromAccountType; - Collection fromAccountTypeOptions = null; - Collection toAccountTypeOptions = null; - - if (accountTransferType.isAccountTransfer()) { - fromAccountTypeOptions = Arrays.asList(savingsAccountType); - toAccountTypeOptions = Arrays.asList(savingsAccountType); - } else if (accountTransferType.isLoanRepayment()) { - fromAccountTypeOptions = Arrays.asList(savingsAccountType); - toAccountTypeOptions = Arrays.asList(loanAccountType); - } else { - fromAccountTypeOptions = Arrays.asList(savingsAccountType, loanAccountType); - toAccountTypeOptions = Arrays.asList(loanAccountType, savingsAccountType); - } - final Integer mostRelevantToAccountType = toAccountType; - - final EnumOptionData fromAccountTypeData = accountType(mostRelevantFromAccountType); - final EnumOptionData toAccountTypeData = accountType(mostRelevantToAccountType); - - // from settings - OfficeData fromOffice = null; - ClientData fromClient = null; - PortfolioAccountData fromAccount = null; - - OfficeData toOffice = null; - ClientData toClient = null; - PortfolioAccountData toAccount = null; - - // template - Collection fromAccountOptions = null; - Collection toAccountOptions = null; - - Long mostRelevantFromOfficeId = fromOfficeId; - Long mostRelevantFromClientId = fromClientId; - - Long mostRelevantToOfficeId = toOfficeId; - Long mostRelevantToClientId = toClientId; - - if (fromAccountId != null) { - Integer accountType; - if (mostRelevantFromAccountType == 1) { - accountType = PortfolioAccountType.LOAN.getValue(); - } else { - accountType = PortfolioAccountType.SAVINGS.getValue(); - } - fromAccount = this.portfolioAccountReadPlatformService.retrieveOne(fromAccountId, accountType); - - // override provided fromClient with client of account - mostRelevantFromClientId = fromAccount.getClientId(); - } - - if (mostRelevantFromClientId != null) { - fromClient = this.clientReadPlatformService.retrieveOne(mostRelevantFromClientId); - mostRelevantFromOfficeId = fromClient.getOfficeId(); - long[] loanStatus = null; - if (mostRelevantFromAccountType == 1) { - loanStatus = new long[] { 300, 700 }; - } - PortfolioAccountDTO portfolioAccountDTO = new PortfolioAccountDTO(mostRelevantFromAccountType, mostRelevantFromClientId, - loanStatus); - fromAccountOptions = this.portfolioAccountReadPlatformService.retrieveAllForLookup(portfolioAccountDTO); - } - - Collection fromOfficeOptions = this.officeReadPlatformService.retrieveAllOfficesForDropdown(); - Collection fromClientOptions = null; - - if (mostRelevantFromOfficeId != null) { - fromOffice = this.officeReadPlatformService.retrieveOffice(mostRelevantFromOfficeId); - fromClientOptions = this.clientReadPlatformService.retrieveAllForLookupByOfficeId(mostRelevantFromOfficeId); - } - - // defaults - final LocalDate transferDate = DateUtils.getBusinessLocalDate(); - Collection toOfficeOptions = fromOfficeOptions; - Collection toClientOptions = null; - - if (toAccountId != null && fromAccount != null) { - toAccount = this.portfolioAccountReadPlatformService.retrieveOne(toAccountId, mostRelevantToAccountType, - fromAccount.getCurrencyCodeFromCurrency()); - mostRelevantToClientId = toAccount.getClientId(); - } - - if (mostRelevantToClientId != null) { - toClient = this.clientReadPlatformService.retrieveOne(mostRelevantToClientId); - mostRelevantToOfficeId = toClient.getOfficeId(); - - toClientOptions = this.clientReadPlatformService.retrieveAllForLookupByOfficeId(mostRelevantToOfficeId); - - toAccountOptions = retrieveToAccounts(fromAccount, mostRelevantToAccountType, mostRelevantToClientId); - } - - if (mostRelevantToOfficeId != null) { - toOffice = this.officeReadPlatformService.retrieveOffice(mostRelevantToOfficeId); - toOfficeOptions = this.officeReadPlatformService.retrieveAllOfficesForDropdown(); - - toClientOptions = this.clientReadPlatformService.retrieveAllForLookupByOfficeId(mostRelevantToOfficeId); - if (toClientOptions != null && toClientOptions.size() == 1) { - toClient = new ArrayList<>(toClientOptions).get(0); - - toAccountOptions = retrieveToAccounts(fromAccount, mostRelevantToAccountType, mostRelevantToClientId); - } - } - - final Collection transferTypeOptions = Arrays.asList(transferType(AccountTransferType.ACCOUNT_TRANSFER), - transferType( - AccountTransferType.LOAN_REPAYMENT)/* - * , transferType( AccountTransferType . CHARGE_PAYMENT ) - */); - final Collection statusOptions = Arrays.asList(standingInstructionStatus(StandingInstructionStatus.ACTIVE), - standingInstructionStatus(StandingInstructionStatus.DISABLED)); - final Collection instructionTypeOptions = Arrays.asList(standingInstructionType(StandingInstructionType.FIXED), - standingInstructionType(StandingInstructionType.DUES)); - final Collection priorityOptions = Arrays.asList(standingInstructionPriority(StandingInstructionPriority.URGENT), - standingInstructionPriority(StandingInstructionPriority.HIGH), - standingInstructionPriority(StandingInstructionPriority.MEDIUM), - standingInstructionPriority(StandingInstructionPriority.LOW)); - final Collection recurrenceTypeOptions = Arrays.asList(recurrenceType(AccountTransferRecurrenceType.PERIODIC), - recurrenceType(AccountTransferRecurrenceType.AS_PER_DUES)); - final Collection recurrenceFrequencyOptions = this.dropdownReadPlatformService.retrievePeriodFrequencyTypeOptions(); - - return StandingInstructionData.template(fromOffice, fromClient, fromAccountTypeData, fromAccount, transferDate, toOffice, toClient, - toAccountTypeData, toAccount, fromOfficeOptions, fromClientOptions, fromAccountTypeOptions, fromAccountOptions, - toOfficeOptions, toClientOptions, toAccountTypeOptions, toAccountOptions, transferTypeOptions, statusOptions, - instructionTypeOptions, priorityOptions, recurrenceTypeOptions, recurrenceFrequencyOptions); - } - - private Collection retrieveToAccounts(final PortfolioAccountData excludeThisAccountFromOptions, - final Integer toAccountType, final Long toClientId) { - - final String currencyCode = excludeThisAccountFromOptions != null ? excludeThisAccountFromOptions.getCurrencyCodeFromCurrency() - : null; - - PortfolioAccountDTO portfolioAccountDTO = new PortfolioAccountDTO(toAccountType, toClientId, currencyCode, null, null); - Collection accountOptions = this.portfolioAccountReadPlatformService - .retrieveAllForLookup(portfolioAccountDTO); - if (!CollectionUtils.isEmpty(accountOptions)) { - accountOptions.remove(excludeThisAccountFromOptions); - } else { - accountOptions = null; - } - - return accountOptions; - } - - @Override - public Page retrieveAll(final StandingInstructionDTO standingInstructionDTO) { - - final StringBuilder sqlBuilder = new StringBuilder(200); - sqlBuilder.append("select " + sqlGenerator.calcFoundRows() + " "); - sqlBuilder.append(this.standingInstructionMapper.schema()); - if (standingInstructionDTO.transferType() != null || standingInstructionDTO.clientId() != null - || standingInstructionDTO.clientName() != null) { - sqlBuilder.append(" where "); - } - boolean addAndCaluse = false; - List paramObj = new ArrayList<>(); - if (standingInstructionDTO.transferType() != null) { - if (addAndCaluse) { - sqlBuilder.append(" and "); - } - sqlBuilder.append(" atd.transfer_type=? "); - paramObj.add(standingInstructionDTO.transferType()); - addAndCaluse = true; - } - if (standingInstructionDTO.clientId() != null) { - if (addAndCaluse) { - sqlBuilder.append(" and "); - } - sqlBuilder.append(" fromclient.id=? "); - paramObj.add(standingInstructionDTO.clientId()); - addAndCaluse = true; - } else if (standingInstructionDTO.clientName() != null) { - if (addAndCaluse) { - sqlBuilder.append(" and "); - } - sqlBuilder.append(" fromclient.display_name=? "); - paramObj.add(standingInstructionDTO.clientName()); - addAndCaluse = true; - } - - if (standingInstructionDTO.fromAccountType() != null && standingInstructionDTO.fromAccount() != null) { - PortfolioAccountType accountType = PortfolioAccountType.fromInt(standingInstructionDTO.fromAccountType()); - if (addAndCaluse) { - sqlBuilder.append(" and "); - } - if (accountType.isSavingsAccount()) { - sqlBuilder.append(" fromsavacc.id=? "); - paramObj.add(standingInstructionDTO.fromAccount()); - } else if (accountType.isLoanAccount()) { - sqlBuilder.append(" fromloanacc.id=? "); - paramObj.add(standingInstructionDTO.fromAccount()); - } - addAndCaluse = true; - } - - final SearchParameters searchParameters = standingInstructionDTO.searchParameters(); - if (searchParameters.hasOrderBy()) { - sqlBuilder.append(" order by ").append(searchParameters.getOrderBy()); - this.columnValidator.validateSqlInjection(sqlBuilder.toString(), searchParameters.getOrderBy()); - if (searchParameters.hasSortOrder()) { - sqlBuilder.append(' ').append(searchParameters.getSortOrder()); - this.columnValidator.validateSqlInjection(sqlBuilder.toString(), searchParameters.getSortOrder()); - } - } - - if (searchParameters.hasLimit()) { - sqlBuilder.append(" "); - if (searchParameters.hasOffset()) { - sqlBuilder.append(sqlGenerator.limit(searchParameters.getLimit(), searchParameters.getOffset())); - } else { - sqlBuilder.append(sqlGenerator.limit(searchParameters.getLimit())); - } - } - - final Object[] finalObjectArray = paramObj.toArray(); - return this.paginationHelper.fetchPage(this.jdbcTemplate, sqlBuilder.toString(), finalObjectArray, this.standingInstructionMapper); - } - - @Override - public Collection retrieveAll(final Integer status) { - final StringBuilder sqlBuilder = new StringBuilder(200); - String businessDate = sqlGenerator.currentBusinessDate(); - sqlBuilder.append("select "); - sqlBuilder.append(this.standingInstructionMapper.schema()); - sqlBuilder - .append(" where atsi.status=? and " + businessDate + " >= atsi.valid_from and (atsi.valid_till IS NULL or " + businessDate - + " < atsi.valid_till) ") - .append(" and (atsi.last_run_date <> " + businessDate + " or atsi.last_run_date IS NULL)") - .append(" ORDER BY atsi.priority DESC"); - return this.jdbcTemplate.query(sqlBuilder.toString(), this.standingInstructionMapper, status); - } - - @Override - public StandingInstructionData retrieveOne(final Long instructionId) { - - try { - final String sql = "select " + this.standingInstructionMapper.schema() + " where atsi.id = ?"; - - return this.jdbcTemplate.queryForObject(sql, this.standingInstructionMapper, new Object[] { instructionId }); // NOSONAR - } catch (final EmptyResultDataAccessException e) { - throw new AccountTransferNotFoundException(instructionId, e); - } - } - - @Override - public StandingInstructionDuesData retriveLoanDuesData(final Long loanId) { - final StandingInstructionLoanDuesMapper rm = new StandingInstructionLoanDuesMapper(); - final String sql = "select " + rm.schema() + " where ml.id= ? and ls.duedate <= " + sqlGenerator.currentBusinessDate() - + " and ls.completed_derived <> 1"; - return this.jdbcTemplate.queryForObject(sql, rm, new Object[] { loanId }); // NOSONAR - } - - private static final class StandingInstructionMapper implements RowMapper { - - private final String schemaSql; - - StandingInstructionMapper() { - final StringBuilder sqlBuilder = new StringBuilder(400); - sqlBuilder.append("atsi.id as id,atsi.name as name, atsi.priority as priority,"); - sqlBuilder.append("atsi.status as status, atsi.instruction_type as instructionType,"); - sqlBuilder.append("atsi.amount as amount,"); - sqlBuilder.append("atsi.valid_from as validFrom, atsi.valid_till as validTill,"); - sqlBuilder.append("atsi.recurrence_type as recurrenceType, atsi.recurrence_frequency as recurrenceFrequency,"); - sqlBuilder.append("atsi.recurrence_interval as recurrenceInterval, atsi.recurrence_on_day as recurrenceOnDay,"); - sqlBuilder.append("atsi.recurrence_on_month as recurrenceOnMonth,"); - sqlBuilder.append("atd.id as accountDetailId,atd.transfer_type as transferType,"); - sqlBuilder.append("fromoff.id as fromOfficeId, fromoff.name as fromOfficeName,"); - sqlBuilder.append("tooff.id as toOfficeId, tooff.name as toOfficeName,"); - sqlBuilder.append("fromclient.id as fromClientId, fromclient.display_name as fromClientName,"); - sqlBuilder.append("toclient.id as toClientId, toclient.display_name as toClientName,"); - sqlBuilder.append("fromsavacc.id as fromSavingsAccountId, fromsavacc.account_no as fromSavingsAccountNo,"); - sqlBuilder.append("fromsp.id as fromProductId, fromsp.name as fromProductName, "); - sqlBuilder.append("fromloanacc.id as fromLoanAccountId, fromloanacc.account_no as fromLoanAccountNo,"); - sqlBuilder.append("fromlp.id as fromLoanProductId, fromlp.name as fromLoanProductName,"); - sqlBuilder.append("tosavacc.id as toSavingsAccountId, tosavacc.account_no as toSavingsAccountNo,"); - sqlBuilder.append("tosp.id as toProductId, tosp.name as toProductName, "); - sqlBuilder.append("toloanacc.id as toLoanAccountId, toloanacc.account_no as toLoanAccountNo, "); - sqlBuilder.append("tolp.id as toLoanProductId, tolp.name as toLoanProductName "); - sqlBuilder.append(" FROM m_account_transfer_standing_instructions atsi "); - sqlBuilder.append("join m_account_transfer_details atd on atd.id = atsi.account_transfer_details_id "); - sqlBuilder.append("join m_office fromoff on fromoff.id = atd.from_office_id "); - sqlBuilder.append("join m_office tooff on tooff.id = atd.to_office_id "); - sqlBuilder.append("join m_client fromclient on fromclient.id = atd.from_client_id "); - sqlBuilder.append("join m_client toclient on toclient.id = atd.to_client_id "); - sqlBuilder.append("left join m_savings_account fromsavacc on fromsavacc.id = atd.from_savings_account_id "); - sqlBuilder.append("left join m_savings_product fromsp ON fromsavacc.product_id = fromsp.id "); - sqlBuilder.append("left join m_loan fromloanacc on fromloanacc.id = atd.from_loan_account_id "); - sqlBuilder.append("left join m_product_loan fromlp ON fromloanacc.product_id = fromlp.id "); - sqlBuilder.append("left join m_savings_account tosavacc on tosavacc.id = atd.to_savings_account_id "); - sqlBuilder.append("left join m_savings_product tosp ON tosavacc.product_id = tosp.id "); - sqlBuilder.append("left join m_loan toloanacc on toloanacc.id = atd.to_loan_account_id "); - sqlBuilder.append("left join m_product_loan tolp ON toloanacc.product_id = tolp.id "); - - this.schemaSql = sqlBuilder.toString(); - } - - public String schema() { - return this.schemaSql; - } - - @Override - public StandingInstructionData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { - - final Long id = rs.getLong("id"); - final Long accountDetailId = rs.getLong("accountDetailId"); - final String name = rs.getString("name"); - final Integer priority = JdbcSupport.getInteger(rs, "priority"); - EnumOptionData priorityEnum = AccountTransferEnumerations.standingInstructionPriority(priority); - - final Integer status = JdbcSupport.getInteger(rs, "status"); - EnumOptionData statusEnum = AccountTransferEnumerations.standingInstructionStatus(status); - final Integer instructionType = JdbcSupport.getInteger(rs, "instructionType"); - EnumOptionData instructionTypeEnum = AccountTransferEnumerations.standingInstructionType(instructionType); - final LocalDate validFrom = JdbcSupport.getLocalDate(rs, "validFrom"); - final LocalDate validTill = JdbcSupport.getLocalDate(rs, "validTill"); - final BigDecimal transferAmount = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "amount"); - final Integer recurrenceType = JdbcSupport.getInteger(rs, "recurrenceType"); - EnumOptionData recurrenceTypeEnum = AccountTransferEnumerations.recurrenceType(recurrenceType); - final Integer recurrenceFrequency = JdbcSupport.getInteger(rs, "recurrenceFrequency"); - - EnumOptionData recurrenceFrequencyEnum = null; - if (recurrenceFrequency != null) { - recurrenceFrequencyEnum = CommonEnumerations.termFrequencyType(recurrenceFrequency, "recurrence"); - } - final Integer recurrenceInterval = JdbcSupport.getInteger(rs, "recurrenceInterval"); - - MonthDay recurrenceOnMonthDay = null; - final Integer recurrenceOnDay = JdbcSupport.getInteger(rs, "recurrenceOnDay"); - final Integer recurrenceOnMonth = JdbcSupport.getInteger(rs, "recurrenceOnMonth"); - if (recurrenceOnDay != null) { - recurrenceOnMonthDay = MonthDay.now(DateUtils.getDateTimeZoneOfTenant()).withMonth(recurrenceOnMonth) - .withDayOfMonth(recurrenceOnDay); - } - - final Integer transferType = rs.getInt("transferType"); - EnumOptionData transferTypeEnum = AccountTransferEnumerations.transferType(transferType); - - /* - * final String currencyCode = rs.getString("currencyCode"); final String currencyName = - * rs.getString("currencyName"); final String currencyNameCode = rs.getString("currencyNameCode"); final - * String currencyDisplaySymbol = rs.getString("currencyDisplaySymbol"); final Integer currencyDigits = - * JdbcSupport.getInteger(rs, "currencyDigits"); final Integer inMultiplesOf = JdbcSupport.getInteger(rs, - * "inMultiplesOf"); final CurrencyData currency = new CurrencyData(currencyCode, currencyName, - * currencyDigits, inMultiplesOf, currencyDisplaySymbol, currencyNameCode); - */ - final Long fromOfficeId = JdbcSupport.getLong(rs, "fromOfficeId"); - final String fromOfficeName = rs.getString("fromOfficeName"); - final OfficeData fromOffice = OfficeData.dropdown(fromOfficeId, fromOfficeName, null); - - final Long toOfficeId = JdbcSupport.getLong(rs, "toOfficeId"); - final String toOfficeName = rs.getString("toOfficeName"); - final OfficeData toOffice = OfficeData.dropdown(toOfficeId, toOfficeName, null); - - final Long fromClientId = JdbcSupport.getLong(rs, "fromClientId"); - final String fromClientName = rs.getString("fromClientName"); - final ClientData fromClient = ClientData.lookup(fromClientId, fromClientName, fromOfficeId, fromOfficeName); - - final Long toClientId = JdbcSupport.getLong(rs, "toClientId"); - final String toClientName = rs.getString("toClientName"); - final ClientData toClient = ClientData.lookup(toClientId, toClientName, toOfficeId, toOfficeName); - - final Long fromSavingsAccountId = JdbcSupport.getLong(rs, "fromSavingsAccountId"); - final String fromSavingsAccountNo = rs.getString("fromSavingsAccountNo"); - final Long fromProductId = JdbcSupport.getLong(rs, "fromProductId"); - final String fromProductName = rs.getString("fromProductName"); - final Long fromLoanAccountId = JdbcSupport.getLong(rs, "fromLoanAccountId"); - final String fromLoanAccountNo = rs.getString("fromLoanAccountNo"); - final Long fromLoanProductId = JdbcSupport.getLong(rs, "fromLoanProductId"); - final String fromLoanProductName = rs.getString("fromLoanProductName"); - PortfolioAccountData fromAccount = null; - EnumOptionData fromAccountType = null; - if (fromSavingsAccountId != null) { - fromAccount = new PortfolioAccountData(fromSavingsAccountId, fromSavingsAccountNo, null, null, null, null, null, - fromProductId, fromProductName, null, null, null); - fromAccountType = accountType(PortfolioAccountType.SAVINGS); - } else if (fromLoanAccountId != null) { - fromAccount = new PortfolioAccountData(fromLoanAccountId, fromLoanAccountNo, null, null, null, null, null, - fromLoanProductId, fromLoanProductName, null, null, null); - fromAccountType = accountType(PortfolioAccountType.LOAN); - } - - PortfolioAccountData toAccount = null; - EnumOptionData toAccountType = null; - final Long toSavingsAccountId = JdbcSupport.getLong(rs, "toSavingsAccountId"); - final String toSavingsAccountNo = rs.getString("toSavingsAccountNo"); - final Long toProductId = JdbcSupport.getLong(rs, "toProductId"); - final String toProductName = rs.getString("toProductName"); - final Long toLoanAccountId = JdbcSupport.getLong(rs, "toLoanAccountId"); - final String toLoanAccountNo = rs.getString("toLoanAccountNo"); - final Long toLoanProductId = JdbcSupport.getLong(rs, "toLoanProductId"); - final String toLoanProductName = rs.getString("toLoanProductName"); - - if (toSavingsAccountId != null) { - toAccount = new PortfolioAccountData(toSavingsAccountId, toSavingsAccountNo, null, null, null, null, null, toProductId, - toProductName, null, null, null); - toAccountType = accountType(PortfolioAccountType.SAVINGS); - } else if (toLoanAccountId != null) { - toAccount = new PortfolioAccountData(toLoanAccountId, toLoanAccountNo, null, null, null, null, null, toLoanProductId, - toLoanProductName, null, null, null); - toAccountType = accountType(PortfolioAccountType.LOAN); - } - - return StandingInstructionData.instance(id, accountDetailId, name, fromOffice, toOffice, fromClient, toClient, fromAccountType, - fromAccount, toAccountType, toAccount, transferTypeEnum, priorityEnum, instructionTypeEnum, statusEnum, transferAmount, - validFrom, validTill, recurrenceTypeEnum, recurrenceFrequencyEnum, recurrenceInterval, recurrenceOnMonthDay); - } - } - - private static final class StandingInstructionLoanDuesMapper implements RowMapper { - - private final String schemaSql; - - StandingInstructionLoanDuesMapper() { - final StringBuilder sqlBuilder = new StringBuilder(400); - - sqlBuilder.append("max(ls.duedate) as dueDate,sum(ls.principal_amount) as principalAmount,"); - sqlBuilder.append("sum(ls.principal_completed_derived) as principalCompleted,"); - sqlBuilder.append("sum(ls.principal_writtenoff_derived) as principalWrittenOff,"); - sqlBuilder.append("sum(ls.interest_amount) as interestAmount,"); - sqlBuilder.append("sum(ls.interest_completed_derived) as interestCompleted,"); - sqlBuilder.append("sum(ls.interest_writtenoff_derived) as interestWrittenOff,"); - sqlBuilder.append("sum(ls.interest_waived_derived) as interestWaived,"); - sqlBuilder.append("sum(ls.penalty_charges_amount) as penalityAmount,"); - sqlBuilder.append("sum(ls.penalty_charges_completed_derived) as penalityCompleted,"); - sqlBuilder.append("sum(ls.penalty_charges_writtenoff_derived)as penaltyWrittenOff,"); - sqlBuilder.append("sum(ls.penalty_charges_waived_derived) as penaltyWaived,"); - sqlBuilder.append("sum(ls.fee_charges_amount) as feeAmount,"); - sqlBuilder.append("sum(ls.fee_charges_completed_derived) as feecompleted,"); - sqlBuilder.append("sum(ls.fee_charges_writtenoff_derived) as feeWrittenOff,"); - sqlBuilder.append("sum(ls.fee_charges_waived_derived) as feeWaived "); - sqlBuilder.append("from m_loan_repayment_schedule ls "); - sqlBuilder.append(" join m_loan ml on ml.id = ls.loan_id "); - - this.schemaSql = sqlBuilder.toString(); - } - - public String schema() { - return this.schemaSql; - } - - @Override - public StandingInstructionDuesData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { - - final LocalDate dueDate = JdbcSupport.getLocalDate(rs, "dueDate"); - final BigDecimal principalDue = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "principalAmount"); - final BigDecimal principalPaid = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "principalCompleted"); - final BigDecimal principalWrittenOff = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "principalWrittenOff"); - final BigDecimal principalOutstanding = principalDue.subtract(principalPaid).subtract(principalWrittenOff); - - final BigDecimal interestExpectedDue = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "interestAmount"); - final BigDecimal interestPaid = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "interestCompleted"); - final BigDecimal interestWrittenOff = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "interestWrittenOff"); - final BigDecimal interestWaived = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "interestWaived"); - final BigDecimal interestActualDue = interestExpectedDue.subtract(interestWaived).subtract(interestWrittenOff); - final BigDecimal interestOutstanding = interestActualDue.subtract(interestPaid); - - final BigDecimal penaltyChargesExpectedDue = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "penalityAmount"); - final BigDecimal penaltyChargesPaid = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "penalityCompleted"); - final BigDecimal penaltyChargesWrittenOff = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "penaltyWrittenOff"); - final BigDecimal penaltyChargesWaived = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "penaltyWaived"); - final BigDecimal penaltyChargesActualDue = penaltyChargesExpectedDue.subtract(penaltyChargesWaived) - .subtract(penaltyChargesWrittenOff); - final BigDecimal penaltyChargesOutstanding = penaltyChargesActualDue.subtract(penaltyChargesPaid); - - final BigDecimal feeChargesExpectedDue = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "feeAmount"); - final BigDecimal feeChargesPaid = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "feecompleted"); - final BigDecimal feeChargesWrittenOff = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "feeWrittenOff"); - final BigDecimal feeChargesWaived = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "feeWaived"); - final BigDecimal feeChargesActualDue = feeChargesExpectedDue.subtract(feeChargesWaived).subtract(feeChargesWrittenOff); - final BigDecimal feeChargesOutstanding = feeChargesActualDue.subtract(feeChargesPaid); - - final BigDecimal totalOutstanding = principalOutstanding.add(interestOutstanding).add(feeChargesOutstanding) - .add(penaltyChargesOutstanding); - - return new StandingInstructionDuesData(dueDate, totalOutstanding); - } - } - -} +/** + * 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.account.service; + +import static org.apache.fineract.portfolio.account.service.AccountTransferEnumerations.accountType; +import static org.apache.fineract.portfolio.account.service.AccountTransferEnumerations.recurrenceType; +import static org.apache.fineract.portfolio.account.service.AccountTransferEnumerations.standingInstructionPriority; +import static org.apache.fineract.portfolio.account.service.AccountTransferEnumerations.standingInstructionStatus; +import static org.apache.fineract.portfolio.account.service.AccountTransferEnumerations.standingInstructionType; +import static org.apache.fineract.portfolio.account.service.AccountTransferEnumerations.transferType; + +import java.math.BigDecimal; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.LocalDate; +import java.time.MonthDay; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import org.apache.fineract.infrastructure.core.data.EnumOptionData; +import org.apache.fineract.infrastructure.core.domain.JdbcSupport; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.Page; +import org.apache.fineract.infrastructure.core.service.PaginationHelper; +import org.apache.fineract.infrastructure.core.service.SearchParameters; +import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; +import org.apache.fineract.infrastructure.security.utils.ColumnValidator; +import org.apache.fineract.organisation.office.data.OfficeData; +import org.apache.fineract.organisation.office.service.OfficeReadPlatformService; +import org.apache.fineract.portfolio.account.PortfolioAccountType; +import org.apache.fineract.portfolio.account.data.PortfolioAccountDTO; +import org.apache.fineract.portfolio.account.data.PortfolioAccountData; +import org.apache.fineract.portfolio.account.data.StandingInstructionDTO; +import org.apache.fineract.portfolio.account.data.StandingInstructionData; +import org.apache.fineract.portfolio.account.data.StandingInstructionDuesData; +import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType; +import org.apache.fineract.portfolio.account.domain.AccountTransferType; +import org.apache.fineract.portfolio.account.domain.StandingInstructionPriority; +import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus; +import org.apache.fineract.portfolio.account.domain.StandingInstructionType; +import org.apache.fineract.portfolio.account.exception.AccountTransferNotFoundException; +import org.apache.fineract.portfolio.client.data.ClientData; +import org.apache.fineract.portfolio.client.service.ClientReadPlatformService; +import org.apache.fineract.portfolio.common.service.CommonEnumerations; +import org.apache.fineract.portfolio.common.service.DropdownReadPlatformService; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.util.CollectionUtils; + +public class StandingInstructionReadPlatformServiceImpl implements StandingInstructionReadPlatformService { + + private final JdbcTemplate jdbcTemplate; + private final ColumnValidator columnValidator; + private final ClientReadPlatformService clientReadPlatformService; + private final OfficeReadPlatformService officeReadPlatformService; + private final PortfolioAccountReadPlatformService portfolioAccountReadPlatformService; + private final DropdownReadPlatformService dropdownReadPlatformService; + private final DatabaseSpecificSQLGenerator sqlGenerator; + + // mapper + private final StandingInstructionMapper standingInstructionMapper; + + // pagination + private final PaginationHelper paginationHelper; + + public StandingInstructionReadPlatformServiceImpl(final JdbcTemplate jdbcTemplate, + final ClientReadPlatformService clientReadPlatformService, final OfficeReadPlatformService officeReadPlatformService, + final PortfolioAccountReadPlatformService portfolioAccountReadPlatformService, + final DropdownReadPlatformService dropdownReadPlatformService, final ColumnValidator columnValidator, + DatabaseSpecificSQLGenerator sqlGenerator, PaginationHelper paginationHelper) { + this.jdbcTemplate = jdbcTemplate; + this.clientReadPlatformService = clientReadPlatformService; + this.officeReadPlatformService = officeReadPlatformService; + this.portfolioAccountReadPlatformService = portfolioAccountReadPlatformService; + this.dropdownReadPlatformService = dropdownReadPlatformService; + this.sqlGenerator = sqlGenerator; + this.standingInstructionMapper = new StandingInstructionMapper(); + this.columnValidator = columnValidator; + this.paginationHelper = paginationHelper; + } + + @Override + public StandingInstructionData retrieveTemplate(final Long fromOfficeId, final Long fromClientId, final Long fromAccountId, + final Integer fromAccountType, final Long toOfficeId, final Long toClientId, final Long toAccountId, + final Integer toAccountType, Integer transferType) { + + AccountTransferType accountTransferType = AccountTransferType.INVALID; + if (transferType != null) { + accountTransferType = AccountTransferType.fromInt(transferType); + } + + final EnumOptionData loanAccountType = accountType(PortfolioAccountType.LOAN); + final EnumOptionData savingsAccountType = accountType(PortfolioAccountType.SAVINGS); + final EnumOptionData currentAccountType = accountType(PortfolioAccountType.CURRENT); + + final Integer mostRelevantFromAccountType = fromAccountType; + Collection fromAccountTypeOptions = null; + Collection toAccountTypeOptions = null; + + if (accountTransferType.isAccountTransfer()) { + fromAccountTypeOptions = Arrays.asList(savingsAccountType, currentAccountType); + toAccountTypeOptions = Arrays.asList(savingsAccountType, currentAccountType); + } else if (accountTransferType.isLoanRepayment()) { + fromAccountTypeOptions = Arrays.asList(savingsAccountType, currentAccountType); + toAccountTypeOptions = Arrays.asList(loanAccountType); + } else { + fromAccountTypeOptions = Arrays.asList(savingsAccountType, currentAccountType, loanAccountType); + toAccountTypeOptions = Arrays.asList(loanAccountType, savingsAccountType, currentAccountType); + } + final Integer mostRelevantToAccountType = toAccountType; + + final EnumOptionData fromAccountTypeData = accountType(mostRelevantFromAccountType); + final EnumOptionData toAccountTypeData = accountType(mostRelevantToAccountType); + + // from settings + OfficeData fromOffice = null; + ClientData fromClient = null; + PortfolioAccountData fromAccount = null; + + OfficeData toOffice = null; + ClientData toClient = null; + PortfolioAccountData toAccount = null; + + // template + Collection fromAccountOptions = null; + Collection toAccountOptions = null; + + Long mostRelevantFromOfficeId = fromOfficeId; + Long mostRelevantFromClientId = fromClientId; + + Long mostRelevantToOfficeId = toOfficeId; + Long mostRelevantToClientId = toClientId; + + if (fromAccountId != null) { + Integer accountType; + if (mostRelevantFromAccountType == 1) { + accountType = PortfolioAccountType.LOAN.getValue(); + } else if (mostRelevantFromAccountType == 3) { + accountType = PortfolioAccountType.CURRENT.getValue(); + } else { + accountType = PortfolioAccountType.SAVINGS.getValue(); + } + fromAccount = this.portfolioAccountReadPlatformService.retrieveOne(fromAccountId, accountType); + + // override provided fromClient with client of account + mostRelevantFromClientId = fromAccount.getClientId(); + } + + if (mostRelevantFromClientId != null) { + fromClient = this.clientReadPlatformService.retrieveOne(mostRelevantFromClientId); + mostRelevantFromOfficeId = fromClient.getOfficeId(); + long[] loanStatus = null; + if (mostRelevantFromAccountType == 1) { + loanStatus = new long[] { 300, 700 }; + } + PortfolioAccountDTO portfolioAccountDTO = new PortfolioAccountDTO(mostRelevantFromAccountType, mostRelevantFromClientId, + loanStatus); + fromAccountOptions = this.portfolioAccountReadPlatformService.retrieveAllForLookup(portfolioAccountDTO); + } + + Collection fromOfficeOptions = this.officeReadPlatformService.retrieveAllOfficesForDropdown(); + Collection fromClientOptions = null; + + if (mostRelevantFromOfficeId != null) { + fromOffice = this.officeReadPlatformService.retrieveOffice(mostRelevantFromOfficeId); + fromClientOptions = this.clientReadPlatformService.retrieveAllForLookupByOfficeId(mostRelevantFromOfficeId); + } + + // defaults + final LocalDate transferDate = DateUtils.getBusinessLocalDate(); + Collection toOfficeOptions = fromOfficeOptions; + Collection toClientOptions = null; + + if (toAccountId != null && fromAccount != null) { + toAccount = this.portfolioAccountReadPlatformService.retrieveOne(toAccountId, mostRelevantToAccountType, + fromAccount.getCurrencyCodeFromCurrency()); + mostRelevantToClientId = toAccount.getClientId(); + } + + if (mostRelevantToClientId != null) { + toClient = this.clientReadPlatformService.retrieveOne(mostRelevantToClientId); + mostRelevantToOfficeId = toClient.getOfficeId(); + + toClientOptions = this.clientReadPlatformService.retrieveAllForLookupByOfficeId(mostRelevantToOfficeId); + + toAccountOptions = retrieveToAccounts(fromAccount, mostRelevantToAccountType, mostRelevantToClientId); + } + + if (mostRelevantToOfficeId != null) { + toOffice = this.officeReadPlatformService.retrieveOffice(mostRelevantToOfficeId); + toOfficeOptions = this.officeReadPlatformService.retrieveAllOfficesForDropdown(); + + toClientOptions = this.clientReadPlatformService.retrieveAllForLookupByOfficeId(mostRelevantToOfficeId); + if (toClientOptions != null && toClientOptions.size() == 1) { + toClient = new ArrayList<>(toClientOptions).get(0); + + toAccountOptions = retrieveToAccounts(fromAccount, mostRelevantToAccountType, mostRelevantToClientId); + } + } + + final Collection transferTypeOptions = Arrays.asList(transferType(AccountTransferType.ACCOUNT_TRANSFER), + transferType( + AccountTransferType.LOAN_REPAYMENT)/* + * , transferType( AccountTransferType . CHARGE_PAYMENT ) + */); + final Collection statusOptions = Arrays.asList(standingInstructionStatus(StandingInstructionStatus.ACTIVE), + standingInstructionStatus(StandingInstructionStatus.DISABLED)); + final Collection instructionTypeOptions = Arrays.asList(standingInstructionType(StandingInstructionType.FIXED), + standingInstructionType(StandingInstructionType.DUES)); + final Collection priorityOptions = Arrays.asList(standingInstructionPriority(StandingInstructionPriority.URGENT), + standingInstructionPriority(StandingInstructionPriority.HIGH), + standingInstructionPriority(StandingInstructionPriority.MEDIUM), + standingInstructionPriority(StandingInstructionPriority.LOW)); + final Collection recurrenceTypeOptions = Arrays.asList(recurrenceType(AccountTransferRecurrenceType.PERIODIC), + recurrenceType(AccountTransferRecurrenceType.AS_PER_DUES)); + final Collection recurrenceFrequencyOptions = this.dropdownReadPlatformService.retrievePeriodFrequencyTypeOptions(); + + return StandingInstructionData.template(fromOffice, fromClient, fromAccountTypeData, fromAccount, transferDate, toOffice, toClient, + toAccountTypeData, toAccount, fromOfficeOptions, fromClientOptions, fromAccountTypeOptions, fromAccountOptions, + toOfficeOptions, toClientOptions, toAccountTypeOptions, toAccountOptions, transferTypeOptions, statusOptions, + instructionTypeOptions, priorityOptions, recurrenceTypeOptions, recurrenceFrequencyOptions); + } + + private Collection retrieveToAccounts(final PortfolioAccountData excludeThisAccountFromOptions, + final Integer toAccountType, final Long toClientId) { + + final String currencyCode = excludeThisAccountFromOptions != null ? excludeThisAccountFromOptions.getCurrencyCodeFromCurrency() + : null; + + PortfolioAccountDTO portfolioAccountDTO = new PortfolioAccountDTO(toAccountType, toClientId, currencyCode, null, null); + Collection accountOptions = this.portfolioAccountReadPlatformService + .retrieveAllForLookup(portfolioAccountDTO); + if (!CollectionUtils.isEmpty(accountOptions)) { + accountOptions.remove(excludeThisAccountFromOptions); + } else { + accountOptions = null; + } + + return accountOptions; + } + + @Override + public Page retrieveAll(final StandingInstructionDTO standingInstructionDTO) { + + final StringBuilder sqlBuilder = new StringBuilder(200); + sqlBuilder.append("select " + sqlGenerator.calcFoundRows() + " "); + sqlBuilder.append(this.standingInstructionMapper.schema()); + if (standingInstructionDTO.transferType() != null || standingInstructionDTO.clientId() != null + || standingInstructionDTO.clientName() != null) { + sqlBuilder.append(" where "); + } + boolean addAndCaluse = false; + List paramObj = new ArrayList<>(); + if (standingInstructionDTO.transferType() != null) { + if (addAndCaluse) { + sqlBuilder.append(" and "); + } + sqlBuilder.append(" atd.transfer_type=? "); + paramObj.add(standingInstructionDTO.transferType()); + addAndCaluse = true; + } + if (standingInstructionDTO.clientId() != null) { + if (addAndCaluse) { + sqlBuilder.append(" and "); + } + sqlBuilder.append(" fromclient.id=? "); + paramObj.add(standingInstructionDTO.clientId()); + addAndCaluse = true; + } else if (standingInstructionDTO.clientName() != null) { + if (addAndCaluse) { + sqlBuilder.append(" and "); + } + sqlBuilder.append(" fromclient.display_name=? "); + paramObj.add(standingInstructionDTO.clientName()); + addAndCaluse = true; + } + + if (standingInstructionDTO.fromAccountType() != null && standingInstructionDTO.fromAccount() != null) { + PortfolioAccountType accountType = PortfolioAccountType.fromInt(standingInstructionDTO.fromAccountType()); + if (addAndCaluse) { + sqlBuilder.append(" and "); + } + if (accountType.isSavingsAccount()) { + sqlBuilder.append(" fromsavacc.id=? "); + paramObj.add(standingInstructionDTO.fromAccount()); + } else if (accountType.isLoanAccount()) { + sqlBuilder.append(" fromloanacc.id=? "); + paramObj.add(standingInstructionDTO.fromAccount()); + } else if (accountType.isCurrentAccount()) { + sqlBuilder.append(" fromcuracc.id=? "); + paramObj.add(standingInstructionDTO.fromAccount()); + } + addAndCaluse = true; + } + + final SearchParameters searchParameters = standingInstructionDTO.searchParameters(); + if (searchParameters.hasOrderBy()) { + sqlBuilder.append(" order by ").append(searchParameters.getOrderBy()); + this.columnValidator.validateSqlInjection(sqlBuilder.toString(), searchParameters.getOrderBy()); + if (searchParameters.hasSortOrder()) { + sqlBuilder.append(' ').append(searchParameters.getSortOrder()); + this.columnValidator.validateSqlInjection(sqlBuilder.toString(), searchParameters.getSortOrder()); + } + } + + if (searchParameters.hasLimit()) { + sqlBuilder.append(" "); + if (searchParameters.hasOffset()) { + sqlBuilder.append(sqlGenerator.limit(searchParameters.getLimit(), searchParameters.getOffset())); + } else { + sqlBuilder.append(sqlGenerator.limit(searchParameters.getLimit())); + } + } + + final Object[] finalObjectArray = paramObj.toArray(); + return this.paginationHelper.fetchPage(this.jdbcTemplate, sqlBuilder.toString(), finalObjectArray, this.standingInstructionMapper); + } + + @Override + public Collection retrieveAll(final Integer status) { + final StringBuilder sqlBuilder = new StringBuilder(200); + String businessDate = sqlGenerator.currentBusinessDate(); + sqlBuilder.append("select "); + sqlBuilder.append(this.standingInstructionMapper.schema()); + sqlBuilder + .append(" where atsi.status=? and " + businessDate + " >= atsi.valid_from and (atsi.valid_till IS NULL or " + businessDate + + " < atsi.valid_till) ") + .append(" and (atsi.last_run_date <> " + businessDate + " or atsi.last_run_date IS NULL)") + .append(" ORDER BY atsi.priority DESC"); + return this.jdbcTemplate.query(sqlBuilder.toString(), this.standingInstructionMapper, status); + } + + @Override + public StandingInstructionData retrieveOne(final Long instructionId) { + + try { + final String sql = "select " + this.standingInstructionMapper.schema() + " where atsi.id = ?"; + + return this.jdbcTemplate.queryForObject(sql, this.standingInstructionMapper, new Object[] { instructionId }); // NOSONAR + } catch (final EmptyResultDataAccessException e) { + throw new AccountTransferNotFoundException(instructionId, e); + } + } + + @Override + public StandingInstructionDuesData retriveLoanDuesData(final Long loanId) { + final StandingInstructionLoanDuesMapper rm = new StandingInstructionLoanDuesMapper(); + final String sql = "select " + rm.schema() + " where ml.id= ? and ls.duedate <= " + sqlGenerator.currentBusinessDate() + + " and ls.completed_derived <> 1"; + return this.jdbcTemplate.queryForObject(sql, rm, new Object[] { loanId }); // NOSONAR + } + + private static final class StandingInstructionMapper implements RowMapper { + + private final String schemaSql; + + StandingInstructionMapper() { + final StringBuilder sqlBuilder = new StringBuilder(400); + sqlBuilder.append("atsi.id as id,atsi.name as name, atsi.priority as priority,"); + sqlBuilder.append("atsi.status as status, atsi.instruction_type as instructionType,"); + sqlBuilder.append("atsi.amount as amount,"); + sqlBuilder.append("atsi.valid_from as validFrom, atsi.valid_till as validTill,"); + sqlBuilder.append("atsi.recurrence_type as recurrenceType, atsi.recurrence_frequency as recurrenceFrequency,"); + sqlBuilder.append("atsi.recurrence_interval as recurrenceInterval, atsi.recurrence_on_day as recurrenceOnDay,"); + sqlBuilder.append("atsi.recurrence_on_month as recurrenceOnMonth,"); + sqlBuilder.append("atd.id as accountDetailId,atd.transfer_type as transferType,"); + sqlBuilder.append("fromoff.id as fromOfficeId, fromoff.name as fromOfficeName,"); + sqlBuilder.append("tooff.id as toOfficeId, tooff.name as toOfficeName,"); + sqlBuilder.append("fromclient.id as fromClientId, fromclient.display_name as fromClientName,"); + sqlBuilder.append("toclient.id as toClientId, toclient.display_name as toClientName,"); + sqlBuilder.append("fromsavacc.id as fromSavingsAccountId, fromsavacc.account_no as fromSavingsAccountNo,"); + sqlBuilder.append("fromsp.id as fromProductId, fromsp.name as fromProductName, "); + sqlBuilder.append("fromloanacc.id as fromLoanAccountId, fromloanacc.account_no as fromLoanAccountNo,"); + sqlBuilder.append("fromlp.id as fromLoanProductId, fromlp.name as fromLoanProductName,"); + sqlBuilder.append("tosavacc.id as toSavingsAccountId, tosavacc.account_no as toSavingsAccountNo,"); + sqlBuilder.append("tosp.id as toProductId, tosp.name as toProductName, "); + sqlBuilder.append("toloanacc.id as toLoanAccountId, toloanacc.account_no as toLoanAccountNo, "); + sqlBuilder.append("tolp.id as toLoanProductId, tolp.name as toLoanProductName, "); + sqlBuilder.append("fromcuracc.id as fromCurrentAccountId, fromcuracc.account_no as fromCurrentAccountNo, "); + sqlBuilder.append("fromcp.id as fromCurrentProductId, fromcp.name as fromCurrentProductName, "); + sqlBuilder.append("tocuracc.id as toCurrentAccountId, tocuracc.account_no as toCurrentAccountNo, "); + sqlBuilder.append("tocp.id as toCurrentProductId, tocp.name as toCurrentProductName "); + sqlBuilder.append(" FROM m_account_transfer_standing_instructions atsi "); + sqlBuilder.append("join m_account_transfer_details atd on atd.id = atsi.account_transfer_details_id "); + sqlBuilder.append("join m_office fromoff on fromoff.id = atd.from_office_id "); + sqlBuilder.append("join m_office tooff on tooff.id = atd.to_office_id "); + sqlBuilder.append("join m_client fromclient on fromclient.id = atd.from_client_id "); + sqlBuilder.append("join m_client toclient on toclient.id = atd.to_client_id "); + sqlBuilder.append("left join m_savings_account fromsavacc on fromsavacc.id = atd.from_savings_account_id "); + sqlBuilder.append("left join m_savings_product fromsp ON fromsavacc.product_id = fromsp.id "); + sqlBuilder.append("left join m_loan fromloanacc on fromloanacc.id = atd.from_loan_account_id "); + sqlBuilder.append("left join m_product_loan fromlp ON fromloanacc.product_id = fromlp.id "); + sqlBuilder.append("left join m_savings_account tosavacc on tosavacc.id = atd.to_savings_account_id "); + sqlBuilder.append("left join m_savings_product tosp ON tosavacc.product_id = tosp.id "); + sqlBuilder.append("left join m_loan toloanacc on toloanacc.id = atd.to_loan_account_id "); + sqlBuilder.append("left join m_product_loan tolp ON toloanacc.product_id = tolp.id "); + sqlBuilder.append("left join m_current_account fromcuracc on fromcuracc.id = atd.from_current_account_id "); + sqlBuilder.append("left join m_current_product fromcp ON fromcuracc.product_id = fromcp.id "); + sqlBuilder.append("left join m_current_account tocuracc on tocuracc.id = atd.to_current_account_id "); + sqlBuilder.append("left join m_current_product tocp ON tocuracc.product_id = tocp.id "); + + this.schemaSql = sqlBuilder.toString(); + } + + public String schema() { + return this.schemaSql; + } + + @Override + public StandingInstructionData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { + + final Long id = rs.getLong("id"); + final Long accountDetailId = rs.getLong("accountDetailId"); + final String name = rs.getString("name"); + final Integer priority = JdbcSupport.getInteger(rs, "priority"); + EnumOptionData priorityEnum = AccountTransferEnumerations.standingInstructionPriority(priority); + + final Integer status = JdbcSupport.getInteger(rs, "status"); + EnumOptionData statusEnum = AccountTransferEnumerations.standingInstructionStatus(status); + final Integer instructionType = JdbcSupport.getInteger(rs, "instructionType"); + EnumOptionData instructionTypeEnum = AccountTransferEnumerations.standingInstructionType(instructionType); + final LocalDate validFrom = JdbcSupport.getLocalDate(rs, "validFrom"); + final LocalDate validTill = JdbcSupport.getLocalDate(rs, "validTill"); + final BigDecimal transferAmount = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "amount"); + final Integer recurrenceType = JdbcSupport.getInteger(rs, "recurrenceType"); + EnumOptionData recurrenceTypeEnum = AccountTransferEnumerations.recurrenceType(recurrenceType); + final Integer recurrenceFrequency = JdbcSupport.getInteger(rs, "recurrenceFrequency"); + + EnumOptionData recurrenceFrequencyEnum = null; + if (recurrenceFrequency != null) { + recurrenceFrequencyEnum = CommonEnumerations.termFrequencyType(recurrenceFrequency, "recurrence"); + } + final Integer recurrenceInterval = JdbcSupport.getInteger(rs, "recurrenceInterval"); + + MonthDay recurrenceOnMonthDay = null; + final Integer recurrenceOnDay = JdbcSupport.getInteger(rs, "recurrenceOnDay"); + final Integer recurrenceOnMonth = JdbcSupport.getInteger(rs, "recurrenceOnMonth"); + if (recurrenceOnDay != null) { + recurrenceOnMonthDay = MonthDay.now(DateUtils.getDateTimeZoneOfTenant()).withMonth(recurrenceOnMonth) + .withDayOfMonth(recurrenceOnDay); + } + + final Integer transferType = rs.getInt("transferType"); + EnumOptionData transferTypeEnum = AccountTransferEnumerations.transferType(transferType); + + /* + * final String currencyCode = rs.getString("currencyCode"); final String currencyName = + * rs.getString("currencyName"); final String currencyNameCode = rs.getString("currencyNameCode"); final + * String currencyDisplaySymbol = rs.getString("currencyDisplaySymbol"); final Integer currencyDigits = + * JdbcSupport.getInteger(rs, "currencyDigits"); final Integer inMultiplesOf = JdbcSupport.getInteger(rs, + * "inMultiplesOf"); final CurrencyData currency = new CurrencyData(currencyCode, currencyName, + * currencyDigits, inMultiplesOf, currencyDisplaySymbol, currencyNameCode); + */ + final Long fromOfficeId = JdbcSupport.getLong(rs, "fromOfficeId"); + final String fromOfficeName = rs.getString("fromOfficeName"); + final OfficeData fromOffice = OfficeData.dropdown(fromOfficeId, fromOfficeName, null); + + final Long toOfficeId = JdbcSupport.getLong(rs, "toOfficeId"); + final String toOfficeName = rs.getString("toOfficeName"); + final OfficeData toOffice = OfficeData.dropdown(toOfficeId, toOfficeName, null); + + final Long fromClientId = JdbcSupport.getLong(rs, "fromClientId"); + final String fromClientName = rs.getString("fromClientName"); + final ClientData fromClient = ClientData.lookup(fromClientId, fromClientName, fromOfficeId, fromOfficeName); + + final Long toClientId = JdbcSupport.getLong(rs, "toClientId"); + final String toClientName = rs.getString("toClientName"); + final ClientData toClient = ClientData.lookup(toClientId, toClientName, toOfficeId, toOfficeName); + + final Long fromSavingsAccountId = JdbcSupport.getLong(rs, "fromSavingsAccountId"); + final String fromSavingsAccountNo = rs.getString("fromSavingsAccountNo"); + final Long fromProductId = JdbcSupport.getLong(rs, "fromProductId"); + final String fromProductName = rs.getString("fromProductName"); + final Long fromLoanAccountId = JdbcSupport.getLong(rs, "fromLoanAccountId"); + final String fromLoanAccountNo = rs.getString("fromLoanAccountNo"); + final Long fromLoanProductId = JdbcSupport.getLong(rs, "fromLoanProductId"); + final String fromLoanProductName = rs.getString("fromLoanProductName"); + PortfolioAccountData fromAccount = null; + EnumOptionData fromAccountType = null; + if (fromSavingsAccountId != null) { + fromAccount = new PortfolioAccountData(fromSavingsAccountId, fromSavingsAccountNo, null, null, null, null, null, + fromProductId, fromProductName, null, null, null); + fromAccountType = accountType(PortfolioAccountType.SAVINGS); + } else if (fromLoanAccountId != null) { + fromAccount = new PortfolioAccountData(fromLoanAccountId, fromLoanAccountNo, null, null, null, null, null, + fromLoanProductId, fromLoanProductName, null, null, null); + fromAccountType = accountType(PortfolioAccountType.LOAN); + } else { + final Long fromCurrentAccountId = JdbcSupport.getLong(rs, "fromCurrentAccountId"); + if (fromCurrentAccountId != null) { + final String fromCurrentAccountNo = rs.getString("fromCurrentAccountNo"); + final Long fromCurrentProductId = JdbcSupport.getLong(rs, "fromCurrentProductId"); + final String fromCurrentProductName = rs.getString("fromCurrentProductName"); + fromAccount = new PortfolioAccountData(fromCurrentAccountId, fromCurrentAccountNo, null, null, null, null, null, + fromCurrentProductId, fromCurrentProductName, null, null, null); + fromAccountType = accountType(PortfolioAccountType.CURRENT); + } + } + + PortfolioAccountData toAccount = null; + EnumOptionData toAccountType = null; + final Long toSavingsAccountId = JdbcSupport.getLong(rs, "toSavingsAccountId"); + final String toSavingsAccountNo = rs.getString("toSavingsAccountNo"); + final Long toProductId = JdbcSupport.getLong(rs, "toProductId"); + final String toProductName = rs.getString("toProductName"); + final Long toLoanAccountId = JdbcSupport.getLong(rs, "toLoanAccountId"); + final String toLoanAccountNo = rs.getString("toLoanAccountNo"); + final Long toLoanProductId = JdbcSupport.getLong(rs, "toLoanProductId"); + final String toLoanProductName = rs.getString("toLoanProductName"); + + if (toSavingsAccountId != null) { + toAccount = new PortfolioAccountData(toSavingsAccountId, toSavingsAccountNo, null, null, null, null, null, toProductId, + toProductName, null, null, null); + toAccountType = accountType(PortfolioAccountType.SAVINGS); + } else if (toLoanAccountId != null) { + toAccount = new PortfolioAccountData(toLoanAccountId, toLoanAccountNo, null, null, null, null, null, toLoanProductId, + toLoanProductName, null, null, null); + toAccountType = accountType(PortfolioAccountType.LOAN); + } else { + final Long toCurrentAccountId = JdbcSupport.getLong(rs, "toCurrentAccountId"); + if (toCurrentAccountId != null) { + final String toCurrentAccountNo = rs.getString("toCurrentAccountNo"); + final Long toCurrentProductId = JdbcSupport.getLong(rs, "toCurrentProductId"); + final String toCurrentProductName = rs.getString("toCurrentProductName"); + toAccount = new PortfolioAccountData(toCurrentAccountId, toCurrentAccountNo, null, null, null, null, null, + toCurrentProductId, toCurrentProductName, null, null, null); + toAccountType = accountType(PortfolioAccountType.CURRENT); + } + } + + return StandingInstructionData.instance(id, accountDetailId, name, fromOffice, toOffice, fromClient, toClient, fromAccountType, + fromAccount, toAccountType, toAccount, transferTypeEnum, priorityEnum, instructionTypeEnum, statusEnum, transferAmount, + validFrom, validTill, recurrenceTypeEnum, recurrenceFrequencyEnum, recurrenceInterval, recurrenceOnMonthDay); + } + } + + private static final class StandingInstructionLoanDuesMapper implements RowMapper { + + private final String schemaSql; + + StandingInstructionLoanDuesMapper() { + final StringBuilder sqlBuilder = new StringBuilder(400); + + sqlBuilder.append("max(ls.duedate) as dueDate,sum(ls.principal_amount) as principalAmount,"); + sqlBuilder.append("sum(ls.principal_completed_derived) as principalCompleted,"); + sqlBuilder.append("sum(ls.principal_writtenoff_derived) as principalWrittenOff,"); + sqlBuilder.append("sum(ls.interest_amount) as interestAmount,"); + sqlBuilder.append("sum(ls.interest_completed_derived) as interestCompleted,"); + sqlBuilder.append("sum(ls.interest_writtenoff_derived) as interestWrittenOff,"); + sqlBuilder.append("sum(ls.interest_waived_derived) as interestWaived,"); + sqlBuilder.append("sum(ls.penalty_charges_amount) as penalityAmount,"); + sqlBuilder.append("sum(ls.penalty_charges_completed_derived) as penalityCompleted,"); + sqlBuilder.append("sum(ls.penalty_charges_writtenoff_derived)as penaltyWrittenOff,"); + sqlBuilder.append("sum(ls.penalty_charges_waived_derived) as penaltyWaived,"); + sqlBuilder.append("sum(ls.fee_charges_amount) as feeAmount,"); + sqlBuilder.append("sum(ls.fee_charges_completed_derived) as feecompleted,"); + sqlBuilder.append("sum(ls.fee_charges_writtenoff_derived) as feeWrittenOff,"); + sqlBuilder.append("sum(ls.fee_charges_waived_derived) as feeWaived "); + sqlBuilder.append("from m_loan_repayment_schedule ls "); + sqlBuilder.append(" join m_loan ml on ml.id = ls.loan_id "); + + this.schemaSql = sqlBuilder.toString(); + } + + public String schema() { + return this.schemaSql; + } + + @Override + public StandingInstructionDuesData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { + + final LocalDate dueDate = JdbcSupport.getLocalDate(rs, "dueDate"); + final BigDecimal principalDue = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "principalAmount"); + final BigDecimal principalPaid = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "principalCompleted"); + final BigDecimal principalWrittenOff = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "principalWrittenOff"); + final BigDecimal principalOutstanding = principalDue.subtract(principalPaid).subtract(principalWrittenOff); + + final BigDecimal interestExpectedDue = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "interestAmount"); + final BigDecimal interestPaid = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "interestCompleted"); + final BigDecimal interestWrittenOff = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "interestWrittenOff"); + final BigDecimal interestWaived = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "interestWaived"); + final BigDecimal interestActualDue = interestExpectedDue.subtract(interestWaived).subtract(interestWrittenOff); + final BigDecimal interestOutstanding = interestActualDue.subtract(interestPaid); + + final BigDecimal penaltyChargesExpectedDue = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "penalityAmount"); + final BigDecimal penaltyChargesPaid = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "penalityCompleted"); + final BigDecimal penaltyChargesWrittenOff = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "penaltyWrittenOff"); + final BigDecimal penaltyChargesWaived = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "penaltyWaived"); + final BigDecimal penaltyChargesActualDue = penaltyChargesExpectedDue.subtract(penaltyChargesWaived) + .subtract(penaltyChargesWrittenOff); + final BigDecimal penaltyChargesOutstanding = penaltyChargesActualDue.subtract(penaltyChargesPaid); + + final BigDecimal feeChargesExpectedDue = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "feeAmount"); + final BigDecimal feeChargesPaid = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "feecompleted"); + final BigDecimal feeChargesWrittenOff = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "feeWrittenOff"); + final BigDecimal feeChargesWaived = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "feeWaived"); + final BigDecimal feeChargesActualDue = feeChargesExpectedDue.subtract(feeChargesWaived).subtract(feeChargesWrittenOff); + final BigDecimal feeChargesOutstanding = feeChargesActualDue.subtract(feeChargesPaid); + + final BigDecimal totalOutstanding = principalOutstanding.add(interestOutstanding).add(feeChargesOutstanding) + .add(penaltyChargesOutstanding); + + return new StandingInstructionDuesData(dueDate, totalOutstanding); + } + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/StandingInstructionWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/StandingInstructionWritePlatformServiceImpl.java index 5e364315e..4ceb701df 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/StandingInstructionWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/StandingInstructionWritePlatformServiceImpl.java @@ -1,145 +1,179 @@ -/** - * 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.account.service; - -import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromAccountTypeParamName; -import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromClientIdParamName; -import static org.apache.fineract.portfolio.account.AccountDetailConstants.toAccountTypeParamName; -import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.statusParamName; - -import java.util.HashMap; -import java.util.Map; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.fineract.infrastructure.core.api.JsonCommand; -import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; -import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; -import org.apache.fineract.infrastructure.core.exception.ErrorHandler; -import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; -import org.apache.fineract.portfolio.account.PortfolioAccountType; -import org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants; -import org.apache.fineract.portfolio.account.data.StandingInstructionDataValidator; -import org.apache.fineract.portfolio.account.domain.AccountTransferDetailRepository; -import org.apache.fineract.portfolio.account.domain.AccountTransferDetails; -import org.apache.fineract.portfolio.account.domain.AccountTransferStandingInstruction; -import org.apache.fineract.portfolio.account.domain.StandingInstructionAssembler; -import org.apache.fineract.portfolio.account.domain.StandingInstructionRepository; -import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus; -import org.apache.fineract.portfolio.account.exception.StandingInstructionNotFoundException; -import org.springframework.dao.DataIntegrityViolationException; -import org.springframework.dao.NonTransientDataAccessException; -import org.springframework.orm.jpa.JpaSystemException; -import org.springframework.transaction.annotation.Transactional; - -@Slf4j -@RequiredArgsConstructor -public class StandingInstructionWritePlatformServiceImpl implements StandingInstructionWritePlatformService { - - private final StandingInstructionDataValidator standingInstructionDataValidator; - private final StandingInstructionAssembler standingInstructionAssembler; - private final AccountTransferDetailRepository accountTransferDetailRepository; - private final StandingInstructionRepository standingInstructionRepository; - - @Transactional - @Override - public CommandProcessingResult create(final JsonCommand command) { - - this.standingInstructionDataValidator.validateForCreate(command); - - final Integer fromAccountTypeId = command.integerValueSansLocaleOfParameterNamed(fromAccountTypeParamName); - final PortfolioAccountType fromAccountType = PortfolioAccountType.fromInt(fromAccountTypeId); - - final Integer toAccountTypeId = command.integerValueSansLocaleOfParameterNamed(toAccountTypeParamName); - final PortfolioAccountType toAccountType = PortfolioAccountType.fromInt(toAccountTypeId); - - final Long fromClientId = command.longValueOfParameterNamed(fromClientIdParamName); - - Long standingInstructionId = null; - try { - if (isSavingsToSavingsAccountTransfer(fromAccountType, toAccountType)) { - final AccountTransferDetails standingInstruction = this.standingInstructionAssembler - .assembleSavingsToSavingsTransfer(command); - this.accountTransferDetailRepository.saveAndFlush(standingInstruction); - standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId(); - } else if (isSavingsToLoanAccountTransfer(fromAccountType, toAccountType)) { - final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleSavingsToLoanTransfer(command); - this.accountTransferDetailRepository.saveAndFlush(standingInstruction); - standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId(); - } else if (isLoanToSavingsAccountTransfer(fromAccountType, toAccountType)) { - - final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleLoanToSavingsTransfer(command); - this.accountTransferDetailRepository.saveAndFlush(standingInstruction); - standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId(); - - } - } catch (final JpaSystemException | DataIntegrityViolationException dve) { - final Throwable throwable = dve.getMostSpecificCause(); - handleDataIntegrityIssues(command, throwable, dve); - return CommandProcessingResult.empty(); - } - final CommandProcessingResultBuilder builder = new CommandProcessingResultBuilder().withEntityId(standingInstructionId) - .withClientId(fromClientId); - return builder.build(); - } - - private void handleDataIntegrityIssues(final JsonCommand command, Throwable realCause, final NonTransientDataAccessException dve) { - if (realCause.getMessage().contains("name")) { - final String name = command.stringValueOfParameterNamed(StandingInstructionApiConstants.nameParamName); - throw new PlatformDataIntegrityException("error.msg.standinginstruction.duplicate.name", - "Standinginstruction with name `" + name + "` already exists", "name", name); - } - log.error("Error occured.", dve); - throw ErrorHandler.getMappable(dve, "error.msg.client.unknown.data.integrity.issue", - "Unknown data integrity issue with resource: " + realCause.getMessage()); - } - - private boolean isLoanToSavingsAccountTransfer(final PortfolioAccountType fromAccountType, final PortfolioAccountType toAccountType) { - return fromAccountType.isLoanAccount() && toAccountType.isSavingsAccount(); - } - - private boolean isSavingsToLoanAccountTransfer(final PortfolioAccountType fromAccountType, final PortfolioAccountType toAccountType) { - return fromAccountType.isSavingsAccount() && toAccountType.isLoanAccount(); - } - - private boolean isSavingsToSavingsAccountTransfer(final PortfolioAccountType fromAccountType, - final PortfolioAccountType toAccountType) { - return fromAccountType.isSavingsAccount() && toAccountType.isSavingsAccount(); - } - - @Override - public CommandProcessingResult update(final Long id, final JsonCommand command) { - this.standingInstructionDataValidator.validateForUpdate(command); - AccountTransferStandingInstruction standingInstructionsForUpdate = this.standingInstructionRepository.findById(id) - .orElseThrow(() -> new StandingInstructionNotFoundException(id)); - final Map actualChanges = standingInstructionsForUpdate.update(command); - return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(id).with(actualChanges).build(); - } - - @Override - public CommandProcessingResult delete(final Long id) { - AccountTransferStandingInstruction standingInstructionsForUpdate = this.standingInstructionRepository.findById(id).orElseThrow(); - // update the "deleted" and "name" properties of the standing - // instruction - standingInstructionsForUpdate.delete(); - - final Map actualChanges = new HashMap<>(); - actualChanges.put(statusParamName, StandingInstructionStatus.DELETED.getValue()); - return new CommandProcessingResultBuilder().withEntityId(id).with(actualChanges).build(); - } -} +/** + * 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.account.service; + +import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromAccountTypeParamName; +import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromClientIdParamName; +import static org.apache.fineract.portfolio.account.AccountDetailConstants.toAccountTypeParamName; +import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.statusParamName; + +import java.util.HashMap; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; +import org.apache.fineract.infrastructure.core.exception.ErrorHandler; +import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; +import org.apache.fineract.portfolio.account.PortfolioAccountType; +import org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants; +import org.apache.fineract.portfolio.account.data.StandingInstructionDataValidator; +import org.apache.fineract.portfolio.account.domain.AccountTransferDetailRepository; +import org.apache.fineract.portfolio.account.domain.AccountTransferDetails; +import org.apache.fineract.portfolio.account.domain.AccountTransferStandingInstruction; +import org.apache.fineract.portfolio.account.domain.StandingInstructionAssembler; +import org.apache.fineract.portfolio.account.domain.StandingInstructionRepository; +import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus; +import org.apache.fineract.portfolio.account.exception.StandingInstructionNotFoundException; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.NonTransientDataAccessException; +import org.springframework.orm.jpa.JpaSystemException; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@RequiredArgsConstructor +public class StandingInstructionWritePlatformServiceImpl implements StandingInstructionWritePlatformService { + + private final StandingInstructionDataValidator standingInstructionDataValidator; + private final StandingInstructionAssembler standingInstructionAssembler; + private final AccountTransferDetailRepository accountTransferDetailRepository; + private final StandingInstructionRepository standingInstructionRepository; + + @Transactional + @Override + public CommandProcessingResult create(final JsonCommand command) { + + this.standingInstructionDataValidator.validateForCreate(command); + + final Integer fromAccountTypeId = command.integerValueSansLocaleOfParameterNamed(fromAccountTypeParamName); + final PortfolioAccountType fromAccountType = PortfolioAccountType.fromInt(fromAccountTypeId); + + final Integer toAccountTypeId = command.integerValueSansLocaleOfParameterNamed(toAccountTypeParamName); + final PortfolioAccountType toAccountType = PortfolioAccountType.fromInt(toAccountTypeId); + + final Long fromClientId = command.longValueOfParameterNamed(fromClientIdParamName); + + Long standingInstructionId = null; + try { + if (isSavingsToSavingsAccountTransfer(fromAccountType, toAccountType)) { + final AccountTransferDetails standingInstruction = this.standingInstructionAssembler + .assembleSavingsToSavingsTransfer(command); + this.accountTransferDetailRepository.saveAndFlush(standingInstruction); + standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId(); + } else if (isSavingsToLoanAccountTransfer(fromAccountType, toAccountType)) { + final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleSavingsToLoanTransfer(command); + this.accountTransferDetailRepository.saveAndFlush(standingInstruction); + standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId(); + } else if (isLoanToSavingsAccountTransfer(fromAccountType, toAccountType)) { + final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleLoanToSavingsTransfer(command); + this.accountTransferDetailRepository.saveAndFlush(standingInstruction); + standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId(); + } else if (isCurrentToCurrentAccountTransfer(fromAccountType, toAccountType)) { + final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleCurrentToCurrentTransfer(command); + this.accountTransferDetailRepository.saveAndFlush(standingInstruction); + standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId(); + } else if (isCurrentToSavingsAccountTransfer(fromAccountType, toAccountType)) { + final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleCurrentToSavingsTransfer(command); + this.accountTransferDetailRepository.saveAndFlush(standingInstruction); + standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId(); + } else if (isSavingsToCurrentAccountTransfer(fromAccountType, toAccountType)) { + final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleSavingsToCurrentTransfer(command); + this.accountTransferDetailRepository.saveAndFlush(standingInstruction); + standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId(); + } else if (isCurrentToLoanAccountTransfer(fromAccountType, toAccountType)) { + final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleCurrentToLoanTransfer(command); + this.accountTransferDetailRepository.saveAndFlush(standingInstruction); + standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId(); + } + } catch (final JpaSystemException | DataIntegrityViolationException dve) { + final Throwable throwable = dve.getMostSpecificCause(); + handleDataIntegrityIssues(command, throwable, dve); + return CommandProcessingResult.empty(); + } + final CommandProcessingResultBuilder builder = new CommandProcessingResultBuilder().withEntityId(standingInstructionId) + .withClientId(fromClientId); + return builder.build(); + } + + private void handleDataIntegrityIssues(final JsonCommand command, Throwable realCause, final NonTransientDataAccessException dve) { + if (realCause.getMessage().contains("name")) { + final String name = command.stringValueOfParameterNamed(StandingInstructionApiConstants.nameParamName); + throw new PlatformDataIntegrityException("error.msg.standinginstruction.duplicate.name", + "Standinginstruction with name `" + name + "` already exists", "name", name); + } + log.error("Error occured.", dve); + throw ErrorHandler.getMappable(dve, "error.msg.client.unknown.data.integrity.issue", + "Unknown data integrity issue with resource: " + realCause.getMessage()); + } + + private boolean isLoanToSavingsAccountTransfer(final PortfolioAccountType fromAccountType, final PortfolioAccountType toAccountType) { + return fromAccountType.isLoanAccount() && toAccountType.isSavingsAccount(); + } + + private boolean isSavingsToLoanAccountTransfer(final PortfolioAccountType fromAccountType, final PortfolioAccountType toAccountType) { + return fromAccountType.isSavingsAccount() && toAccountType.isLoanAccount(); + } + + private boolean isSavingsToSavingsAccountTransfer(final PortfolioAccountType fromAccountType, + final PortfolioAccountType toAccountType) { + return fromAccountType.isSavingsAccount() && toAccountType.isSavingsAccount(); + } + + private boolean isCurrentToCurrentAccountTransfer(final PortfolioAccountType fromAccountType, + final PortfolioAccountType toAccountType) { + return fromAccountType.isCurrentAccount() && toAccountType.isCurrentAccount(); + } + + private boolean isCurrentToSavingsAccountTransfer(final PortfolioAccountType fromAccountType, + final PortfolioAccountType toAccountType) { + return fromAccountType.isCurrentAccount() && toAccountType.isSavingsAccount(); + } + + private boolean isSavingsToCurrentAccountTransfer(final PortfolioAccountType fromAccountType, + final PortfolioAccountType toAccountType) { + return fromAccountType.isSavingsAccount() && toAccountType.isCurrentAccount(); + } + + private boolean isCurrentToLoanAccountTransfer(final PortfolioAccountType fromAccountType, + final PortfolioAccountType toAccountType) { + return fromAccountType.isCurrentAccount() && toAccountType.isLoanAccount(); + } + + @Override + public CommandProcessingResult update(final Long id, final JsonCommand command) { + this.standingInstructionDataValidator.validateForUpdate(command); + AccountTransferStandingInstruction standingInstructionsForUpdate = this.standingInstructionRepository.findById(id) + .orElseThrow(() -> new StandingInstructionNotFoundException(id)); + final Map actualChanges = standingInstructionsForUpdate.update(command); + return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(id).with(actualChanges).build(); + } + + @Override + public CommandProcessingResult delete(final Long id) { + AccountTransferStandingInstruction standingInstructionsForUpdate = this.standingInstructionRepository.findById(id).orElseThrow(); + // update the "deleted" and "name" properties of the standing + // instruction + standingInstructionsForUpdate.delete(); + + final Map actualChanges = new HashMap<>(); + actualChanges.put(statusParamName, StandingInstructionStatus.DELETED.getValue()); + return new CommandProcessingResultBuilder().withEntityId(id).with(actualChanges).build(); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/starter/AccountConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/starter/AccountConfiguration.java index 412b78faa..c2bfdc826 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/starter/AccountConfiguration.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/starter/AccountConfiguration.java @@ -1,134 +1,138 @@ -/** - * 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.account.starter; - -import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; -import org.apache.fineract.infrastructure.core.config.FineractProperties; -import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; -import org.apache.fineract.infrastructure.core.service.PaginationHelper; -import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; -import org.apache.fineract.infrastructure.security.service.SqlValidator; -import org.apache.fineract.infrastructure.security.utils.ColumnValidator; -import org.apache.fineract.organisation.office.service.OfficeReadPlatformService; -import org.apache.fineract.portfolio.account.data.AccountTransfersDataValidator; -import org.apache.fineract.portfolio.account.data.StandingInstructionDataValidator; -import org.apache.fineract.portfolio.account.domain.AccountTransferAssembler; -import org.apache.fineract.portfolio.account.domain.AccountTransferDetailRepository; -import org.apache.fineract.portfolio.account.domain.AccountTransferRepository; -import org.apache.fineract.portfolio.account.domain.StandingInstructionAssembler; -import org.apache.fineract.portfolio.account.domain.StandingInstructionRepository; -import org.apache.fineract.portfolio.account.mapper.AccountTransfersMapper; -import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlatformService; -import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlatformServiceImpl; -import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService; -import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformServiceImpl; -import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService; -import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformServiceImpl; -import org.apache.fineract.portfolio.account.service.PortfolioAccountReadPlatformService; -import org.apache.fineract.portfolio.account.service.PortfolioAccountReadPlatformServiceImpl; -import org.apache.fineract.portfolio.account.service.StandingInstructionHistoryReadPlatformService; -import org.apache.fineract.portfolio.account.service.StandingInstructionHistoryReadPlatformServiceImpl; -import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformService; -import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformServiceImpl; -import org.apache.fineract.portfolio.account.service.StandingInstructionWritePlatformService; -import org.apache.fineract.portfolio.account.service.StandingInstructionWritePlatformServiceImpl; -import org.apache.fineract.portfolio.client.service.ClientReadPlatformService; -import org.apache.fineract.portfolio.common.service.DropdownReadPlatformService; -import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService; -import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler; -import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService; -import org.apache.fineract.portfolio.savings.domain.GSIMRepositoy; -import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler; -import org.apache.fineract.portfolio.savings.service.SavingsAccountDomainService; -import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.jdbc.core.JdbcTemplate; - -@Configuration -public class AccountConfiguration { - - @Bean - @ConditionalOnMissingBean(AccountAssociationsReadPlatformService.class) - public AccountAssociationsReadPlatformService accountAssociationsReadPlatformService(JdbcTemplate jdbcTemplate) { - return new AccountAssociationsReadPlatformServiceImpl(jdbcTemplate); - } - - @Bean - @ConditionalOnMissingBean(AccountTransfersReadPlatformService.class) - public AccountTransfersReadPlatformService accountTransfersReadPlatformService(JdbcTemplate jdbcTemplate, - ClientReadPlatformService clientReadPlatformService, OfficeReadPlatformService officeReadPlatformService, - PortfolioAccountReadPlatformService portfolioAccountReadPlatformService, ColumnValidator columnValidator, - DatabaseSpecificSQLGenerator sqlGenerator, AccountTransfersMapper accountTransfersMapper, PaginationHelper paginationHelper, - SqlValidator sqlValidator) { - return new AccountTransfersReadPlatformServiceImpl(jdbcTemplate, clientReadPlatformService, officeReadPlatformService, - portfolioAccountReadPlatformService, columnValidator, sqlGenerator, accountTransfersMapper, paginationHelper, sqlValidator); - } - - @Bean - @ConditionalOnMissingBean(AccountTransfersWritePlatformService.class) - public AccountTransfersWritePlatformService accountTransfersWritePlatformService( - AccountTransfersDataValidator accountTransfersDataValidator, AccountTransferAssembler accountTransferAssembler, - AccountTransferRepository accountTransferRepository, SavingsAccountAssembler savingsAccountAssembler, - SavingsAccountDomainService savingsAccountDomainService, LoanAssembler loanAccountAssembler, - LoanAccountDomainService loanAccountDomainService, SavingsAccountWritePlatformService savingsAccountWritePlatformService, - AccountTransferDetailRepository accountTransferDetailRepository, LoanReadPlatformService loanReadPlatformService, - GSIMRepositoy gsimRepository, ConfigurationDomainService configurationDomainService, ExternalIdFactory externalIdFactory, - FineractProperties fineractProperties) { - return new AccountTransfersWritePlatformServiceImpl(accountTransfersDataValidator, accountTransferAssembler, - accountTransferRepository, savingsAccountAssembler, savingsAccountDomainService, loanAccountAssembler, - loanAccountDomainService, savingsAccountWritePlatformService, accountTransferDetailRepository, loanReadPlatformService, - gsimRepository, configurationDomainService, externalIdFactory, fineractProperties); - } - - @Bean - @ConditionalOnMissingBean(PortfolioAccountReadPlatformService.class) - public PortfolioAccountReadPlatformService portfolioAccountReadPlatformService(JdbcTemplate jdbcTemplate, - DatabaseSpecificSQLGenerator sqlGenerator) { - return new PortfolioAccountReadPlatformServiceImpl(jdbcTemplate, sqlGenerator); - } - - @Bean - @ConditionalOnMissingBean(StandingInstructionHistoryReadPlatformService.class) - public StandingInstructionHistoryReadPlatformService standingInstructionHistoryReadPlatformService(JdbcTemplate jdbcTemplate, - ColumnValidator columnValidator, DatabaseSpecificSQLGenerator sqlGenerator, PaginationHelper paginationHelper) { - return new StandingInstructionHistoryReadPlatformServiceImpl(jdbcTemplate, columnValidator, sqlGenerator, paginationHelper); - } - - @Bean - @ConditionalOnMissingBean(StandingInstructionReadPlatformService.class) - public StandingInstructionReadPlatformService standingInstructionReadPlatformService(JdbcTemplate jdbcTemplate, - ClientReadPlatformService clientReadPlatformService, OfficeReadPlatformService officeReadPlatformService, - PortfolioAccountReadPlatformService portfolioAccountReadPlatformService, - DropdownReadPlatformService dropdownReadPlatformService, ColumnValidator columnValidator, - DatabaseSpecificSQLGenerator sqlGenerator, PaginationHelper paginationHelper) { - return new StandingInstructionReadPlatformServiceImpl(jdbcTemplate, clientReadPlatformService, officeReadPlatformService, - portfolioAccountReadPlatformService, dropdownReadPlatformService, columnValidator, sqlGenerator, paginationHelper); - } - - @Bean - @ConditionalOnMissingBean(StandingInstructionWritePlatformService.class) - public StandingInstructionWritePlatformService standingInstructionWritePlatformService( - StandingInstructionDataValidator standingInstructionDataValidator, StandingInstructionAssembler standingInstructionAssembler, - AccountTransferDetailRepository accountTransferDetailRepository, StandingInstructionRepository standingInstructionRepository) { - return new StandingInstructionWritePlatformServiceImpl(standingInstructionDataValidator, standingInstructionAssembler, - accountTransferDetailRepository, standingInstructionRepository); - } -} +/** + * 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.account.starter; + +import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; +import org.apache.fineract.infrastructure.core.config.FineractProperties; +import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; +import org.apache.fineract.infrastructure.core.service.PaginationHelper; +import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; +import org.apache.fineract.infrastructure.security.service.SqlValidator; +import org.apache.fineract.infrastructure.security.utils.ColumnValidator; +import org.apache.fineract.organisation.office.service.OfficeReadPlatformService; +import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountRepository; +import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountTransactionRepository; +import org.apache.fineract.portfolio.account.data.AccountTransfersDataValidator; +import org.apache.fineract.portfolio.account.data.StandingInstructionDataValidator; +import org.apache.fineract.portfolio.account.domain.AccountTransferAssembler; +import org.apache.fineract.portfolio.account.domain.AccountTransferDetailRepository; +import org.apache.fineract.portfolio.account.domain.AccountTransferRepository; +import org.apache.fineract.portfolio.account.domain.StandingInstructionAssembler; +import org.apache.fineract.portfolio.account.domain.StandingInstructionRepository; +import org.apache.fineract.portfolio.account.mapper.AccountTransfersMapper; +import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlatformService; +import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlatformServiceImpl; +import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService; +import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformServiceImpl; +import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService; +import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformServiceImpl; +import org.apache.fineract.portfolio.account.service.PortfolioAccountReadPlatformService; +import org.apache.fineract.portfolio.account.service.PortfolioAccountReadPlatformServiceImpl; +import org.apache.fineract.portfolio.account.service.StandingInstructionHistoryReadPlatformService; +import org.apache.fineract.portfolio.account.service.StandingInstructionHistoryReadPlatformServiceImpl; +import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformService; +import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformServiceImpl; +import org.apache.fineract.portfolio.account.service.StandingInstructionWritePlatformService; +import org.apache.fineract.portfolio.account.service.StandingInstructionWritePlatformServiceImpl; +import org.apache.fineract.portfolio.client.service.ClientReadPlatformService; +import org.apache.fineract.portfolio.common.service.DropdownReadPlatformService; +import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService; +import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler; +import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService; +import org.apache.fineract.portfolio.savings.domain.GSIMRepositoy; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler; +import org.apache.fineract.portfolio.savings.service.SavingsAccountDomainService; +import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.core.JdbcTemplate; + +@Configuration +public class AccountConfiguration { + + @Bean + @ConditionalOnMissingBean(AccountAssociationsReadPlatformService.class) + public AccountAssociationsReadPlatformService accountAssociationsReadPlatformService(JdbcTemplate jdbcTemplate) { + return new AccountAssociationsReadPlatformServiceImpl(jdbcTemplate); + } + + @Bean + @ConditionalOnMissingBean(AccountTransfersReadPlatformService.class) + public AccountTransfersReadPlatformService accountTransfersReadPlatformService(JdbcTemplate jdbcTemplate, + ClientReadPlatformService clientReadPlatformService, OfficeReadPlatformService officeReadPlatformService, + PortfolioAccountReadPlatformService portfolioAccountReadPlatformService, ColumnValidator columnValidator, + DatabaseSpecificSQLGenerator sqlGenerator, AccountTransfersMapper accountTransfersMapper, PaginationHelper paginationHelper, + SqlValidator sqlValidator) { + return new AccountTransfersReadPlatformServiceImpl(jdbcTemplate, clientReadPlatformService, officeReadPlatformService, + portfolioAccountReadPlatformService, columnValidator, sqlGenerator, accountTransfersMapper, paginationHelper, sqlValidator); + } + + @Bean + @ConditionalOnMissingBean(AccountTransfersWritePlatformService.class) + public AccountTransfersWritePlatformService accountTransfersWritePlatformService( + AccountTransfersDataValidator accountTransfersDataValidator, AccountTransferAssembler accountTransferAssembler, + AccountTransferRepository accountTransferRepository, SavingsAccountAssembler savingsAccountAssembler, + SavingsAccountDomainService savingsAccountDomainService, LoanAssembler loanAccountAssembler, + LoanAccountDomainService loanAccountDomainService, SavingsAccountWritePlatformService savingsAccountWritePlatformService, + AccountTransferDetailRepository accountTransferDetailRepository, LoanReadPlatformService loanReadPlatformService, + GSIMRepositoy gsimRepository, ConfigurationDomainService configurationDomainService, ExternalIdFactory externalIdFactory, + FineractProperties fineractProperties, CurrentAccountRepository currentAccountRepository, + CurrentAccountTransactionRepository currentAccountTransactionRepository) { + return new AccountTransfersWritePlatformServiceImpl(accountTransfersDataValidator, accountTransferAssembler, + accountTransferRepository, savingsAccountAssembler, savingsAccountDomainService, loanAccountAssembler, + loanAccountDomainService, savingsAccountWritePlatformService, accountTransferDetailRepository, loanReadPlatformService, + gsimRepository, configurationDomainService, externalIdFactory, fineractProperties, + currentAccountRepository, currentAccountTransactionRepository); + } + + @Bean + @ConditionalOnMissingBean(PortfolioAccountReadPlatformService.class) + public PortfolioAccountReadPlatformService portfolioAccountReadPlatformService(JdbcTemplate jdbcTemplate, + DatabaseSpecificSQLGenerator sqlGenerator) { + return new PortfolioAccountReadPlatformServiceImpl(jdbcTemplate, sqlGenerator); + } + + @Bean + @ConditionalOnMissingBean(StandingInstructionHistoryReadPlatformService.class) + public StandingInstructionHistoryReadPlatformService standingInstructionHistoryReadPlatformService(JdbcTemplate jdbcTemplate, + ColumnValidator columnValidator, DatabaseSpecificSQLGenerator sqlGenerator, PaginationHelper paginationHelper) { + return new StandingInstructionHistoryReadPlatformServiceImpl(jdbcTemplate, columnValidator, sqlGenerator, paginationHelper); + } + + @Bean + @ConditionalOnMissingBean(StandingInstructionReadPlatformService.class) + public StandingInstructionReadPlatformService standingInstructionReadPlatformService(JdbcTemplate jdbcTemplate, + ClientReadPlatformService clientReadPlatformService, OfficeReadPlatformService officeReadPlatformService, + PortfolioAccountReadPlatformService portfolioAccountReadPlatformService, + DropdownReadPlatformService dropdownReadPlatformService, ColumnValidator columnValidator, + DatabaseSpecificSQLGenerator sqlGenerator, PaginationHelper paginationHelper) { + return new StandingInstructionReadPlatformServiceImpl(jdbcTemplate, clientReadPlatformService, officeReadPlatformService, + portfolioAccountReadPlatformService, dropdownReadPlatformService, columnValidator, sqlGenerator, paginationHelper); + } + + @Bean + @ConditionalOnMissingBean(StandingInstructionWritePlatformService.class) + public StandingInstructionWritePlatformService standingInstructionWritePlatformService( + StandingInstructionDataValidator standingInstructionDataValidator, StandingInstructionAssembler standingInstructionAssembler, + AccountTransferDetailRepository accountTransferDetailRepository, StandingInstructionRepository standingInstructionRepository) { + return new StandingInstructionWritePlatformServiceImpl(standingInstructionDataValidator, standingInstructionAssembler, + accountTransferDetailRepository, standingInstructionRepository); + } +} diff --git a/fineract-provider/src/main/resources/application.properties b/fineract-provider/src/main/resources/application.properties index 696393b9e..3432b0ff3 100644 --- a/fineract-provider/src/main/resources/application.properties +++ b/fineract-provider/src/main/resources/application.properties @@ -407,7 +407,7 @@ server.tomcat.mbeanregistry.enabled=${FINERACT_SERVER_TOMCAT_MBEANREGISTRY_ENABL spring.datasource.hikari.driverClassName=${FINERACT_HIKARI_DRIVER_SOURCE_CLASS_NAME:org.postgresql.Driver} spring.datasource.hikari.jdbcUrl=${FINERACT_HIKARI_JDBC_URL:jdbc:postgresql://localhost:5432/fineract_tenants} spring.datasource.hikari.username=${FINERACT_HIKARI_USERNAME:postgres} -spring.datasource.hikari.password=${FINERACT_HIKARI_PASSWORD:root} +spring.datasource.hikari.password=${FINERACT_HIKARI_PASSWORD:313863} spring.datasource.hikari.minimumIdle=${FINERACT_HIKARI_MINIMUM_IDLE:3} spring.datasource.hikari.maximumPoolSize=${FINERACT_HIKARI_MAXIMUM_POOL_SIZE:10} spring.datasource.hikari.idleTimeout=${FINERACT_HIKARI_IDLE_TIMEOUT:60000} diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index 592ae3103..e640e4ace 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -317,4 +317,5 @@ + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0301_add_current_account_to_transfer_details.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0301_add_current_account_to_transfer_details.xml new file mode 100644 index 000000000..5a93b8059 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0301_add_current_account_to_transfer_details.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 7d4f409202128c075bf235c338aedc571206fa02 Mon Sep 17 00:00:00 2001 From: Jaanu423 Date: Tue, 26 May 2026 16:30:04 +0530 Subject: [PATCH 04/10] feat: fix standing instructions GL accounting for current account transfers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AccountTransfersWritePlatformServiceImpl: * Direct GL posting for current↔savings/current transfers: DR debit-side account GL (with correct account entity_id) CR credit-side account GL (with correct account entity_id) * Prefix current account txn_id with CA (not C) so JournalEntryReadPlatformServiceImpl joins m_current_account and shows correct current account number on GL entries * Suppress redundant savings deposit/withdrawal GL entries (skipAccountingPostings=true) when current account handles the full double-entry directly * Separate performCurrentWithdrawalForLoan for Current→Loan transfers (loan domain service handles the other GL leg) * Reuse existing AccountTransferDetails for SI recurring runs so transactions appear in list-account-transactions history - SavingsAccountDomainService / SavingsAccountDomainServiceJpa: * New handleDeposit overload with skipAccountingPostings flag * handleWithdrawal respects SavingsTransactionBooleanValues .isSkipAccountingPostings() to suppress redundant GL entries - SavingsTransactionBooleanValues: * Added skipAccountingPostings field + overloaded constructor - AccountingProcessorHelper: * New createDirectTransferJournalEntriesForSavings() method creates DR and CR entries with separate productId/entityId so each GL line shows the correct account number - JournalEntryReadPlatformServiceImpl: * Join both m_savings_account and m_current_account always * COALESCE(ca.account_no, sa.account_no) to resolve correct account number regardless of transaction_id prefix - ExecuteStandingInstructionsTasklet: * Load existing AccountTransferDetails via repository so subsequent SI runs link to the same transfer detail record - ExecuteStandingInstructionsConfig / AccountConfiguration: * Wire AccountTransferDetailRepository and AccountingProcessorHelper into the relevant beans Co-Authored-By: Claude Sonnet 4.6 --- .../service/AccountingProcessorHelper.java | 21 + .../JournalEntryReadPlatformServiceImpl.java | 7 +- .../ExecuteStandingInstructionsConfig.java | 143 ++-- .../ExecuteStandingInstructionsTasklet.java | 10 +- ...ountTransfersWritePlatformServiceImpl.java | 182 ++++- .../account/starter/AccountConfiguration.java | 6 +- .../SavingsAccountDomainServiceJpa.java | 689 +++++++++--------- .../SavingsTransactionBooleanValues.java | 129 ++-- .../service/SavingsAccountDomainService.java | 106 +-- 9 files changed, 738 insertions(+), 555 deletions(-) diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java index ee76982f5..c2ffffe6d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java @@ -600,6 +600,27 @@ public class AccountingProcessorHelper { createCreditJournalEntryForSavings(office, currencyCode, creditAccount, savingsId, transactionId, transactionDate, amount); } + /** + * Creates a direct transfer journal entry between two savings-type accounts (savings or current), + * each with their own product GL mapping and entity ID so the correct account number appears per entry. + * + * @param debitProductId product ID whose SAVINGS_CONTROL GL is debited + * @param debitEntityId entity (account) ID shown on the debit entry + * @param creditProductId product ID whose SAVINGS_CONTROL GL is credited + * @param creditEntityId entity (account) ID shown on the credit entry + */ + public void createDirectTransferJournalEntriesForSavings(final Office office, final String currencyCode, + final Long debitProductId, final Long debitEntityId, + final Long creditProductId, final Long creditEntityId, + final String transactionId, final LocalDate transactionDate, final BigDecimal amount) { + final GLAccount debitAccount = getLinkedGLAccountForSavingsProduct(debitProductId, + CashAccountsForSavings.SAVINGS_CONTROL.getValue(), null); + final GLAccount creditAccount = getLinkedGLAccountForSavingsProduct(creditProductId, + CashAccountsForSavings.SAVINGS_CONTROL.getValue(), null); + createDebitJournalEntryForSavings(office, currencyCode, debitAccount, debitEntityId, transactionId, transactionDate, amount); + createCreditJournalEntryForSavings(office, currencyCode, creditAccount, creditEntityId, transactionId, transactionDate, amount); + } + /** * Convenience method that creates a pair of related Debits and Credits for Cash Based accounting. * diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryReadPlatformServiceImpl.java index 519edb532..66f577291 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryReadPlatformServiceImpl.java @@ -106,8 +106,7 @@ public class JournalEntryReadPlatformServiceImpl implements JournalEntryReadPlat .append(" curr.display_symbol as currencyDisplaySymbol, curr.decimal_places as currencyDigits, curr.currency_multiplesof as inMultiplesOf, ") .append(" eao.external_id as externalAssetOwner, ") .append(" CASE ") - .append(" WHEN journalEntry.entity_type_enum = 2 AND journalEntry.transaction_id LIKE 'CA%' THEN ca.account_no ") - .append(" WHEN journalEntry.entity_type_enum = 2 THEN sa.account_no ") + .append(" WHEN journalEntry.entity_type_enum = 2 THEN COALESCE(ca.account_no, sa.account_no) ") .append(" ELSE NULL ") .append(" END as savingsAccountNo "); if (associationParametersData.isRunningBalanceRequired()) { @@ -130,8 +129,8 @@ public class JournalEntryReadPlatformServiceImpl implements JournalEntryReadPlat .append(" join m_currency curr on curr.code = journalEntry.currency_code ") .append(" left join m_external_asset_owner_journal_entry_mapping eajem on eajem.journal_entry_id = journalEntry.id ") .append(" left join m_external_asset_owner eao on eao.id = eajem.owner_id ") - .append(" left join m_savings_account sa on sa.id = journalEntry.entity_id AND journalEntry.entity_type_enum = 2 AND journalEntry.transaction_id NOT LIKE 'CA%' ") - .append(" left join m_current_account ca on ca.id = journalEntry.entity_id AND journalEntry.entity_type_enum = 2 AND journalEntry.transaction_id LIKE 'CA%' "); + .append(" left join m_savings_account sa on sa.id = journalEntry.entity_id AND journalEntry.entity_type_enum = 2 ") + .append(" left join m_current_account ca on ca.id = journalEntry.entity_id AND journalEntry.entity_type_enum = 2 "); if (associationParametersData.isTransactionDetailsRequired()) { sb.append(" left join m_loan_transaction as lt on journalEntry.loan_transaction_id = lt.id ") .append(" left join m_savings_account_transaction as st on journalEntry.savings_transaction_id = st.id ") diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/jobs/executestandinginstructions/ExecuteStandingInstructionsConfig.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/jobs/executestandinginstructions/ExecuteStandingInstructionsConfig.java index 69fecbdb4..d7a120589 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/jobs/executestandinginstructions/ExecuteStandingInstructionsConfig.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/jobs/executestandinginstructions/ExecuteStandingInstructionsConfig.java @@ -1,70 +1,73 @@ -/** - * 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.account.jobs.executestandinginstructions; - -import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; -import org.apache.fineract.infrastructure.jobs.service.JobName; -import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService; -import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformService; -import org.springframework.batch.core.Job; -import org.springframework.batch.core.Step; -import org.springframework.batch.core.job.builder.JobBuilder; -import org.springframework.batch.core.launch.support.RunIdIncrementer; -import org.springframework.batch.core.repository.JobRepository; -import org.springframework.batch.core.step.builder.StepBuilder; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.transaction.PlatformTransactionManager; - -@Configuration -public class ExecuteStandingInstructionsConfig { - - @Autowired - private JobRepository jobRepository; - @Autowired - private PlatformTransactionManager transactionManager; - @Autowired - private StandingInstructionReadPlatformService standingInstructionReadPlatformService; - @Autowired - private JdbcTemplate jdbcTemplate; - @Autowired - private DatabaseSpecificSQLGenerator sqlGenerator; - @Autowired - private AccountTransfersWritePlatformService accountTransfersWritePlatformService; - - @Bean - protected Step executeStandingInstructionsStep() { - return new StepBuilder(JobName.EXECUTE_STANDING_INSTRUCTIONS.name(), jobRepository) - .tasklet(executeStandingInstructionsTasklet(), transactionManager).build(); - } - - @Bean - public Job executeStandingInstructionsJob() { - return new JobBuilder(JobName.EXECUTE_STANDING_INSTRUCTIONS.name(), jobRepository).start(executeStandingInstructionsStep()) - .incrementer(new RunIdIncrementer()).build(); - } - - @Bean - public ExecuteStandingInstructionsTasklet executeStandingInstructionsTasklet() { - return new ExecuteStandingInstructionsTasklet(standingInstructionReadPlatformService, jdbcTemplate, sqlGenerator, - accountTransfersWritePlatformService); - } -} +/** + * 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.account.jobs.executestandinginstructions; + +import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; +import org.apache.fineract.infrastructure.jobs.service.JobName; +import org.apache.fineract.portfolio.account.domain.AccountTransferDetailRepository; +import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService; +import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformService; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.launch.support.RunIdIncrementer; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.transaction.PlatformTransactionManager; + +@Configuration +public class ExecuteStandingInstructionsConfig { + + @Autowired + private JobRepository jobRepository; + @Autowired + private PlatformTransactionManager transactionManager; + @Autowired + private StandingInstructionReadPlatformService standingInstructionReadPlatformService; + @Autowired + private JdbcTemplate jdbcTemplate; + @Autowired + private DatabaseSpecificSQLGenerator sqlGenerator; + @Autowired + private AccountTransfersWritePlatformService accountTransfersWritePlatformService; + @Autowired + private AccountTransferDetailRepository accountTransferDetailRepository; + + @Bean + protected Step executeStandingInstructionsStep() { + return new StepBuilder(JobName.EXECUTE_STANDING_INSTRUCTIONS.name(), jobRepository) + .tasklet(executeStandingInstructionsTasklet(), transactionManager).build(); + } + + @Bean + public Job executeStandingInstructionsJob() { + return new JobBuilder(JobName.EXECUTE_STANDING_INSTRUCTIONS.name(), jobRepository).start(executeStandingInstructionsStep()) + .incrementer(new RunIdIncrementer()).build(); + } + + @Bean + public ExecuteStandingInstructionsTasklet executeStandingInstructionsTasklet() { + return new ExecuteStandingInstructionsTasklet(standingInstructionReadPlatformService, jdbcTemplate, sqlGenerator, + accountTransfersWritePlatformService, accountTransferDetailRepository); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/jobs/executestandinginstructions/ExecuteStandingInstructionsTasklet.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/jobs/executestandinginstructions/ExecuteStandingInstructionsTasklet.java index 352c012e3..cb3f87297 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/jobs/executestandinginstructions/ExecuteStandingInstructionsTasklet.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/jobs/executestandinginstructions/ExecuteStandingInstructionsTasklet.java @@ -34,6 +34,8 @@ import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException; import org.apache.fineract.portfolio.account.data.AccountTransferDTO; import org.apache.fineract.portfolio.account.data.StandingInstructionData; import org.apache.fineract.portfolio.account.data.StandingInstructionDuesData; +import org.apache.fineract.portfolio.account.domain.AccountTransferDetailRepository; +import org.apache.fineract.portfolio.account.domain.AccountTransferDetails; import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType; import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus; import org.apache.fineract.portfolio.account.domain.StandingInstructionType; @@ -58,6 +60,7 @@ public class ExecuteStandingInstructionsTasklet implements Tasklet { private final JdbcTemplate jdbcTemplate; private final DatabaseSpecificSQLGenerator sqlGenerator; private final AccountTransfersWritePlatformService accountTransfersWritePlatformService; + private final AccountTransferDetailRepository accountTransferDetailRepository; @Override public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { @@ -105,10 +108,15 @@ public class ExecuteStandingInstructionsTasklet implements Tasklet { final SavingsAccount fromSavingsAccount = null; final boolean isRegularTransaction = true; final boolean isExceptionForBalanceCheck = false; + // Load existing AccountTransferDetails so transactions are linked to + // this standing instruction and appear in its transaction history + final AccountTransferDetails existingDetails = data.getAccountDetailId() != null + ? accountTransferDetailRepository.findById(data.getAccountDetailId()).orElse(null) + : null; AccountTransferDTO accountTransferDTO = new AccountTransferDTO(transactionDate, transactionAmount, data.fromAccountTypeEnum(), data.toAccountTypeEnum(), data.getFromAccount().getId(), data.getToAccount().getId(), data.getName() + " Standing instruction trasfer ", null, null, null, null, data.toTransferType(), null, null, - data.transferTypeEnum().getValue(), null, null, ExternalId.empty(), null, null, fromSavingsAccount, + data.transferTypeEnum().getValue(), existingDetails, null, ExternalId.empty(), null, null, fromSavingsAccount, isRegularTransaction, isExceptionForBalanceCheck); final boolean transferCompleted = transferAmount(errors, accountTransferDTO, data.getId()); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransfersWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransfersWritePlatformServiceImpl.java index aaf24bd3d..1ae645c56 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransfersWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransfersWritePlatformServiceImpl.java @@ -34,6 +34,9 @@ import java.util.Collection; import java.util.List; import java.util.Locale; import lombok.RequiredArgsConstructor; +import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForSavings; +import org.apache.fineract.accounting.common.AccountingConstants.FinancialActivity; +import org.apache.fineract.accounting.journalentry.service.AccountingProcessorHelper; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.config.FineractProperties; @@ -96,6 +99,7 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer private final FineractProperties fineractProperties; private final CurrentAccountRepository currentAccountRepository; private final CurrentAccountTransactionRepository currentAccountTransactionRepository; + private final AccountingProcessorHelper accountingProcessorHelper; @Transactional @Override @@ -467,23 +471,29 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer .assembleFrom(accountTransferDTO.getToAccountId(), backdatedTxnsAllowedTill); final CurrentAccountTransaction withdrawal = performCurrentWithdrawal(fromCurrentAccount, - accountTransferDTO.getTransactionAmount(), accountTransferDTO.getTransactionDate()); + accountTransferDTO.getTransactionAmount(), accountTransferDTO.getTransactionDate(), + toSavingsAccount.productId(), toSavingsAccount.getId()); + // skipAccountingPostings=true: current account withdrawal leg (C-txn) already posts + // DR CURRENT ACCOUNT / CR SAVING INDIVIDUAL directly — each with its own account number. + // Savings deposit GL entries would produce a self-cancelling DR+CR on SAVING INDIVIDUAL — suppress them. final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount, accountTransferDTO.getFmt(), accountTransferDTO.getTransactionDate(), accountTransferDTO.getTransactionAmount(), accountTransferDTO.getPaymentDetail(), - isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill); + isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill, true); - final AccountTransferDetails newDetails = AccountTransferDetails.currentToSavingsTransfer( - fromCurrentAccount.getOffice(), fromCurrentAccount.getClient(), fromCurrentAccount, - toSavingsAccount.office(), toSavingsAccount.getClient(), toSavingsAccount, - accountTransferDTO.getTransferType()); + if (accountTransferDetails == null) { + accountTransferDetails = AccountTransferDetails.currentToSavingsTransfer( + fromCurrentAccount.getOffice(), fromCurrentAccount.getClient(), fromCurrentAccount, + toSavingsAccount.office(), toSavingsAccount.getClient(), toSavingsAccount, + accountTransferDTO.getTransferType()); + } final Money amount = Money.of(toSavingsAccount.getCurrency(), accountTransferDTO.getTransactionAmount()); - newDetails.addAccountTransferTransaction(AccountTransferTransaction.savingsToSavingsTransfer( - newDetails, null, deposit, accountTransferDTO.getTransactionDate(), amount, + accountTransferDetails.addAccountTransferTransaction(AccountTransferTransaction.savingsToSavingsTransfer( + accountTransferDetails, null, deposit, accountTransferDTO.getTransactionDate(), amount, accountTransferDTO.getDescription())); - this.accountTransferDetailRepository.saveAndFlush(newDetails); - transferTransactionId = newDetails.getId(); + this.accountTransferDetailRepository.saveAndFlush(accountTransferDetails); + transferTransactionId = accountTransferDetails.getId(); } else if (isSavingsToCurrentAccountTransfer(accountTransferDTO.getFromAccountType(), accountTransferDTO.getToAccountType())) { // --- Savings Account → Current Account --- @@ -494,27 +504,33 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer .orElseThrow(() -> new GeneralPlatformDomainRuleException( "error.msg.currentaccount.not.found", "Current account not found: " + accountTransferDTO.getToAccountId())); + // skipAccountingPostings=true: current account deposit leg (C-txn) posts + // DR SAVING INDIVIDUAL / CR CURRENT ACCOUNT directly. Savings withdrawal GL entries + // would produce a self-cancelling DR+CR on the same SAVING INDIVIDUAL GL — suppress them. final SavingsTransactionBooleanValues txnBooleans = new SavingsTransactionBooleanValues(isAccountTransfer, isRegularTransaction, fromSavingsAccount.isWithdrawalFeeApplicableForTransfer(), false, - accountTransferDTO.isExceptionForBalanceCheck()); + accountTransferDTO.isExceptionForBalanceCheck(), true); final SavingsAccountTransaction withdrawal = this.savingsAccountDomainService.handleWithdrawal(fromSavingsAccount, accountTransferDTO.getFmt(), accountTransferDTO.getTransactionDate(), accountTransferDTO.getTransactionAmount(), accountTransferDTO.getPaymentDetail(), txnBooleans, backdatedTxnsAllowedTill); performCurrentDeposit(toCurrentAccount, accountTransferDTO.getTransactionAmount(), - accountTransferDTO.getTransactionDate()); + accountTransferDTO.getTransactionDate(), + fromSavingsAccount.productId(), fromSavingsAccount.getId()); - final AccountTransferDetails newDetails = AccountTransferDetails.savingsToCurrentTransfer( - fromSavingsAccount.office(), fromSavingsAccount.getClient(), fromSavingsAccount, - toCurrentAccount.getOffice(), toCurrentAccount.getClient(), toCurrentAccount, - accountTransferDTO.getTransferType()); + if (accountTransferDetails == null) { + accountTransferDetails = AccountTransferDetails.savingsToCurrentTransfer( + fromSavingsAccount.office(), fromSavingsAccount.getClient(), fromSavingsAccount, + toCurrentAccount.getOffice(), toCurrentAccount.getClient(), toCurrentAccount, + accountTransferDTO.getTransferType()); + } final Money amount = Money.of(fromSavingsAccount.getCurrency(), accountTransferDTO.getTransactionAmount()); - newDetails.addAccountTransferTransaction(AccountTransferTransaction.savingsToSavingsTransfer( - newDetails, withdrawal, null, accountTransferDTO.getTransactionDate(), amount, + accountTransferDetails.addAccountTransferTransaction(AccountTransferTransaction.savingsToSavingsTransfer( + accountTransferDetails, withdrawal, null, accountTransferDTO.getTransactionDate(), amount, accountTransferDTO.getDescription())); - this.accountTransferDetailRepository.saveAndFlush(newDetails); - transferTransactionId = newDetails.getId(); + this.accountTransferDetailRepository.saveAndFlush(accountTransferDetails); + transferTransactionId = accountTransferDetails.getId(); } else if (isCurrentToCurrentAccountTransfer(accountTransferDTO.getFromAccountType(), accountTransferDTO.getToAccountType())) { // --- Current Account → Current Account --- @@ -528,22 +544,26 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer "error.msg.currentaccount.not.found", "Current account not found: " + accountTransferDTO.getToAccountId())); performCurrentWithdrawal(fromCurrentAccount, accountTransferDTO.getTransactionAmount(), - accountTransferDTO.getTransactionDate()); + accountTransferDTO.getTransactionDate(), + toCurrentAccount.getProductId(), toCurrentAccount.getId()); performCurrentDeposit(toCurrentAccount, accountTransferDTO.getTransactionAmount(), - accountTransferDTO.getTransactionDate()); + accountTransferDTO.getTransactionDate(), + fromCurrentAccount.getProductId(), fromCurrentAccount.getId()); - final AccountTransferDetails newDetails = AccountTransferDetails.currentToCurrentTransfer( - fromCurrentAccount.getOffice(), fromCurrentAccount.getClient(), fromCurrentAccount, - toCurrentAccount.getOffice(), toCurrentAccount.getClient(), toCurrentAccount, - accountTransferDTO.getTransferType()); + if (accountTransferDetails == null) { + accountTransferDetails = AccountTransferDetails.currentToCurrentTransfer( + fromCurrentAccount.getOffice(), fromCurrentAccount.getClient(), fromCurrentAccount, + toCurrentAccount.getOffice(), toCurrentAccount.getClient(), toCurrentAccount, + accountTransferDTO.getTransferType()); + } final MonetaryCurrency currency = new MonetaryCurrency(fromCurrentAccount.getCurrencyCode(), fromCurrentAccount.getCurrencyDigits(), fromCurrentAccount.getCurrencyMultiplesOf()); final Money amount = Money.of(currency, accountTransferDTO.getTransactionAmount()); - newDetails.addAccountTransferTransaction(AccountTransferTransaction.savingsToSavingsTransfer( - newDetails, null, null, accountTransferDTO.getTransactionDate(), amount, + accountTransferDetails.addAccountTransferTransaction(AccountTransferTransaction.savingsToSavingsTransfer( + accountTransferDetails, null, null, accountTransferDTO.getTransactionDate(), amount, accountTransferDTO.getDescription())); - this.accountTransferDetailRepository.saveAndFlush(newDetails); - transferTransactionId = newDetails.getId(); + this.accountTransferDetailRepository.saveAndFlush(accountTransferDetails); + transferTransactionId = accountTransferDetails.getId(); } else if (isCurrentToLoanAccountTransfer(accountTransferDTO.getFromAccountType(), accountTransferDTO.getToAccountType())) { // --- Current Account → Loan Account --- @@ -553,7 +573,7 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer "error.msg.currentaccount.not.found", "Current account not found: " + accountTransferDTO.getFromAccountId())); final Loan toLoanAccount = this.loanAccountAssembler.assembleFrom(accountTransferDTO.getToAccountId()); - performCurrentWithdrawal(fromCurrentAccount, accountTransferDTO.getTransactionAmount(), + performCurrentWithdrawalForLoan(fromCurrentAccount, accountTransferDTO.getTransactionAmount(), accountTransferDTO.getTransactionDate()); ExternalId externalId = externalIdFactory.create(accountTransferDTO.getTxnExternalId().getValue()); @@ -656,10 +676,10 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer } /** - * Withdraw amount from a current account and save the transaction. - * Throws {@link GeneralPlatformDomainRuleException} if balance is insufficient. + * Withdraw for Current → Loan transfers. Loan domain service handles the loan-side GL, + * so current account side uses LIABILITY_TRANSFER as the counterpart (classic approach). */ - private CurrentAccountTransaction performCurrentWithdrawal(final CurrentAccount account, final BigDecimal amount, + private CurrentAccountTransaction performCurrentWithdrawalForLoan(final CurrentAccount account, final BigDecimal amount, final LocalDate txnDate) { final BigDecimal balance = account.getAccountBalanceDerived() != null ? account.getAccountBalanceDerived() : BigDecimal.ZERO; final BigDecimal onHold = account.getOnHoldFundsDerived() != null ? account.getOnHoldFundsDerived() : BigDecimal.ZERO; @@ -677,6 +697,40 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer account.setTotalWithdrawalsDerived( (account.getTotalWithdrawalsDerived() != null ? account.getTotalWithdrawalsDerived() : BigDecimal.ZERO).add(amount)); currentAccountRepository.save(account); + if (account.getProductId() != null && txn.getId() != null) { + final String currentTxnId = "CA" + txn.getId(); + accountingProcessorHelper.createCashBasedJournalEntriesAndReversalsForSavings(account.getOffice(), + account.getCurrencyCode(), CashAccountsForSavings.SAVINGS_CONTROL.getValue(), + FinancialActivity.LIABILITY_TRANSFER.getValue(), account.getProductId(), null, + account.getId(), currentTxnId, txnDate, amount, false); + } + return txn; + } + + /** + * Withdraw amount from a current account and save the transaction. + * Throws {@link GeneralPlatformDomainRuleException} if balance is insufficient. + */ + private CurrentAccountTransaction performCurrentWithdrawal(final CurrentAccount account, final BigDecimal amount, + final LocalDate txnDate, final Long counterpartProductId, final Long counterpartEntityId) { + final BigDecimal balance = account.getAccountBalanceDerived() != null ? account.getAccountBalanceDerived() : BigDecimal.ZERO; + final BigDecimal onHold = account.getOnHoldFundsDerived() != null ? account.getOnHoldFundsDerived() : BigDecimal.ZERO; + final BigDecimal available = balance.subtract(onHold); + if (!account.isAllowOverdraft() && amount.compareTo(available) > 0) { + throw new GeneralPlatformDomainRuleException("error.msg.current.account.insufficient.balance", + "Insufficient balance in current account " + account.getId() + + ". Available: " + available + ", Requested: " + amount, account.getId()); + } + final CurrentAccountTransaction txn = CurrentAccountTransaction.withdrawal(account, account.getOffice(), amount, txnDate, null); + final BigDecimal newBalance = balance.subtract(amount); + txn.setRunningBalanceDerived(newBalance); + currentAccountTransactionRepository.saveAndFlush(txn); + account.setAccountBalanceDerived(newBalance); + account.setTotalWithdrawalsDerived( + (account.getTotalWithdrawalsDerived() != null ? account.getTotalWithdrawalsDerived() : BigDecimal.ZERO).add(amount)); + currentAccountRepository.save(account); + // Post journal entries: DR current account GL, CR counterpart GL — each with correct entity/account number + postCurrentAccountTransferJournalEntries(account, txn, amount, txnDate, true, counterpartProductId, counterpartEntityId); return txn; } @@ -684,7 +738,7 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer * Deposit amount into a current account and save the transaction. */ private CurrentAccountTransaction performCurrentDeposit(final CurrentAccount account, final BigDecimal amount, - final LocalDate txnDate) { + final LocalDate txnDate, final Long counterpartProductId, final Long counterpartEntityId) { final BigDecimal balance = account.getAccountBalanceDerived() != null ? account.getAccountBalanceDerived() : BigDecimal.ZERO; final CurrentAccountTransaction txn = CurrentAccountTransaction.deposit(account, account.getOffice(), amount, txnDate, null); final BigDecimal newBalance = balance.add(amount); @@ -694,9 +748,67 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer account.setTotalDepositsDerived( (account.getTotalDepositsDerived() != null ? account.getTotalDepositsDerived() : BigDecimal.ZERO).add(amount)); currentAccountRepository.save(account); + // Post journal entries: DR counterpart GL, CR current account GL — each with correct entity/account number + postCurrentAccountTransferJournalEntries(account, txn, amount, txnDate, false, counterpartProductId, counterpartEntityId); return txn; } + /** + * Posts double-entry journal entries for a current account leg of an account transfer. + *

+ * Current Product GL mappings are stored under {@code PortfolioProductType.SAVING}, so the + * savings JE helper resolves them correctly. + *

+ * Transaction id is prefixed with "C" (e.g. {@code C57}) so the entries are NOT grouped with + * savings entries that share the same numeric id (those use prefix "S"). The helper only + * appends its own "S" prefix when the id is numeric; passing "C57" keeps it as-is. + * + * @param isWithdrawal {@code true} for the from-leg (DR control / CR liability transfer); + * {@code false} for the to-leg (DR liability transfer / CR control) + */ + /** + * Posts direct transfer journal entries between a current account and a counterpart savings/current account. + * Each entry carries the correct entity ID so the correct account number appears on each GL line. + * + * For withdrawal (current → savings): + * DR: current product GL (entity = current account) + * CR: counterpart product GL (entity = savings/current account) + * + * For deposit (savings → current): + * DR: counterpart product GL (entity = savings/current account) + * CR: current product GL (entity = current account) + * + * @param counterpartProductId savings/current product ID of the other account + * @param counterpartEntityId savings/current account ID of the other account + */ + private void postCurrentAccountTransferJournalEntries(final CurrentAccount account, final CurrentAccountTransaction txn, + final BigDecimal amount, final LocalDate txnDate, final boolean isWithdrawal, + final Long counterpartProductId, final Long counterpartEntityId) { + if (account.getProductId() == null || txn.getId() == null) { + return; + } + // Prefix "CA" so: + // 1. The helper does NOT re-prefix with "S" (avoids txn id collision with savings "S" prefix) + // 2. JournalEntryReadPlatformServiceImpl joins m_current_account (transaction_id LIKE 'CA%') + // so the correct current account number appears on the DR/CR entry. + final String currentTxnId = "CA" + txn.getId(); + if (isWithdrawal) { + // DR: current account GL CR: counterpart (savings/current) GL + accountingProcessorHelper.createDirectTransferJournalEntriesForSavings( + account.getOffice(), account.getCurrencyCode(), + account.getProductId(), account.getId(), + counterpartProductId, counterpartEntityId, + currentTxnId, txnDate, amount); + } else { + // DR: counterpart (savings/current) GL CR: current account GL + accountingProcessorHelper.createDirectTransferJournalEntriesForSavings( + account.getOffice(), account.getCurrencyCode(), + counterpartProductId, counterpartEntityId, + account.getProductId(), account.getId(), + currentTxnId, txnDate, amount); + } + } + @Override @Transactional public CommandProcessingResult refundByTransfer(JsonCommand command) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/starter/AccountConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/starter/AccountConfiguration.java index c2bfdc826..58be11f63 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/starter/AccountConfiguration.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/starter/AccountConfiguration.java @@ -18,6 +18,7 @@ */ package org.apache.fineract.portfolio.account.starter; +import org.apache.fineract.accounting.journalentry.service.AccountingProcessorHelper; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.core.config.FineractProperties; import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; @@ -94,12 +95,13 @@ public class AccountConfiguration { AccountTransferDetailRepository accountTransferDetailRepository, LoanReadPlatformService loanReadPlatformService, GSIMRepositoy gsimRepository, ConfigurationDomainService configurationDomainService, ExternalIdFactory externalIdFactory, FineractProperties fineractProperties, CurrentAccountRepository currentAccountRepository, - CurrentAccountTransactionRepository currentAccountTransactionRepository) { + CurrentAccountTransactionRepository currentAccountTransactionRepository, + AccountingProcessorHelper accountingProcessorHelper) { return new AccountTransfersWritePlatformServiceImpl(accountTransfersDataValidator, accountTransferAssembler, accountTransferRepository, savingsAccountAssembler, savingsAccountDomainService, loanAccountAssembler, loanAccountDomainService, savingsAccountWritePlatformService, accountTransferDetailRepository, loanReadPlatformService, gsimRepository, configurationDomainService, externalIdFactory, fineractProperties, - currentAccountRepository, currentAccountTransactionRepository); + currentAccountRepository, currentAccountTransactionRepository, accountingProcessorHelper); } @Bean diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountDomainServiceJpa.java index fb16621dc..80b9c1a0d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountDomainServiceJpa.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountDomainServiceJpa.java @@ -1,333 +1,356 @@ -/** - * 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 java.math.BigDecimal; -import java.math.MathContext; -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; -import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService; -import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; -import org.apache.fineract.infrastructure.core.service.DateUtils; -import org.apache.fineract.infrastructure.event.business.domain.savings.transaction.SavingsDepositBusinessEvent; -import org.apache.fineract.infrastructure.event.business.domain.savings.transaction.SavingsWithdrawalBusinessEvent; -import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; -import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; -import org.apache.fineract.organisation.monetary.domain.ApplicationCurrencyRepositoryWrapper; -import org.apache.fineract.organisation.monetary.domain.Money; -import org.apache.fineract.organisation.monetary.domain.MoneyHelper; -import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail; -import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType; -import org.apache.fineract.portfolio.savings.SavingsTransactionBooleanValues; -import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionDTO; -import org.apache.fineract.portfolio.savings.exception.DepositAccountTransactionNotAllowedException; -import org.apache.fineract.portfolio.savings.service.SavingsAccountDomainService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Service -public class SavingsAccountDomainServiceJpa implements SavingsAccountDomainService { - - private final PlatformSecurityContext context; - private final SavingsAccountRepositoryWrapper savingsAccountRepository; - private final SavingsAccountTransactionRepository savingsAccountTransactionRepository; - private final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepositoryWrapper; - private final JournalEntryWritePlatformService journalEntryWritePlatformService; - private final ConfigurationDomainService configurationDomainService; - private final DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository; - private final BusinessEventNotifierService businessEventNotifierService; - - @Autowired - public SavingsAccountDomainServiceJpa(final SavingsAccountRepositoryWrapper savingsAccountRepository, - final SavingsAccountTransactionRepository savingsAccountTransactionRepository, - final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepositoryWrapper, - final JournalEntryWritePlatformService journalEntryWritePlatformService, - final ConfigurationDomainService configurationDomainService, final PlatformSecurityContext context, - final DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository, - final BusinessEventNotifierService businessEventNotifierService) { - this.savingsAccountRepository = savingsAccountRepository; - this.savingsAccountTransactionRepository = savingsAccountTransactionRepository; - this.applicationCurrencyRepositoryWrapper = applicationCurrencyRepositoryWrapper; - this.journalEntryWritePlatformService = journalEntryWritePlatformService; - this.configurationDomainService = configurationDomainService; - this.context = context; - this.depositAccountOnHoldTransactionRepository = depositAccountOnHoldTransactionRepository; - this.businessEventNotifierService = businessEventNotifierService; - } - - @Transactional - @Override - public SavingsAccountTransaction handleWithdrawal(final SavingsAccount account, final DateTimeFormatter fmt, - final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail, - final SavingsTransactionBooleanValues transactionBooleanValues, final boolean backdatedTxnsAllowedTill) { - context.authenticatedUser(); - account.validateForAccountBlock(); - account.validateForDebitBlock(); - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - final Long relaxingDaysConfigForPivotDate = this.configurationDomainService.retrieveRelaxingDaysConfigForPivotDate(); - final boolean postReversals = this.configurationDomainService.isReversalTransactionAllowed(); - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - if (transactionBooleanValues.isRegularTransaction() && !account.allowWithdrawal()) { - throw new DepositAccountTransactionNotAllowedException(account.getId(), "withdraw", account.depositAccountType()); - } - final Set existingTransactionIds = new HashSet<>(); - final LocalDate postInterestOnDate = null; - final Set existingReversedTransactionIds = new HashSet<>(); - - if (backdatedTxnsAllowedTill) { - updateTransactionDetailsWithPivotConfig(account, existingTransactionIds, existingReversedTransactionIds); - } else { - updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); - } - - Integer accountType = null; - final SavingsAccountTransactionDTO transactionDTO = new SavingsAccountTransactionDTO(fmt, transactionDate, transactionAmount, - paymentDetail, null, accountType); - UUID refNo = UUID.randomUUID(); - final SavingsAccountTransaction withdrawal = account.withdraw(transactionDTO, transactionBooleanValues.isApplyWithdrawFee(), - backdatedTxnsAllowedTill, relaxingDaysConfigForPivotDate, refNo.toString()); - final MathContext mc = MathContext.DECIMAL64; - - final LocalDate today = DateUtils.getBusinessLocalDate(); - - if (account.isBeforeLastPostingPeriod(transactionDate, backdatedTxnsAllowedTill)) { - account.postInterest(mc, today, transactionBooleanValues.isInterestTransfer(), isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals); - } else { - account.calculateInterestUsing(mc, today, transactionBooleanValues.isInterestTransfer(), - isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, - postReversals); - } - - List depositAccountOnHoldTransactions = null; - if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { - depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository - .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); - } - - account.validateAccountBalanceDoesNotBecomeNegative(transactionAmount, transactionBooleanValues.isExceptionForBalanceCheck(), - depositAccountOnHoldTransactions, backdatedTxnsAllowedTill); - - saveTransactionToGenerateTransactionId(withdrawal); - if (backdatedTxnsAllowedTill) { - // Update transactions separately - saveUpdatedTransactionsOfSavingsAccount(account.getSavingsAccountTransactionsWithPivotConfig()); - } - this.savingsAccountRepository.save(account); - - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, transactionBooleanValues.isAccountTransfer(), - backdatedTxnsAllowedTill); - - businessEventNotifierService.notifyPostBusinessEvent(new SavingsWithdrawalBusinessEvent(withdrawal)); - return withdrawal; - } - - @Transactional - @Override - public SavingsAccountTransaction handleDeposit(final SavingsAccount account, final DateTimeFormatter fmt, - final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail, - final boolean isAccountTransfer, final boolean isRegularTransaction, final boolean backdatedTxnsAllowedTill) { - final SavingsAccountTransactionType savingsAccountTransactionType = SavingsAccountTransactionType.DEPOSIT; - return handleDeposit(account, fmt, transactionDate, transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction, - savingsAccountTransactionType, backdatedTxnsAllowedTill); - } - - private SavingsAccountTransaction handleDeposit(final SavingsAccount account, final DateTimeFormatter fmt, - final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail, - final boolean isAccountTransfer, final boolean isRegularTransaction, - final SavingsAccountTransactionType savingsAccountTransactionType, final boolean backdatedTxnsAllowedTill) { - context.authenticatedUser(); - account.validateForAccountBlock(); - account.validateForCreditBlock(); - - // Global configurations - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - final Long relaxingDaysConfigForPivotDate = this.configurationDomainService.retrieveRelaxingDaysConfigForPivotDate(); - if (isRegularTransaction && !account.allowDeposit()) { - throw new DepositAccountTransactionNotAllowedException(account.getId(), "deposit", account.depositAccountType()); - } - boolean isInterestTransfer = false; - final Set existingTransactionIds = new HashSet<>(); - final Set existingReversedTransactionIds = new HashSet<>(); - - if (backdatedTxnsAllowedTill) { - updateTransactionDetailsWithPivotConfig(account, existingTransactionIds, existingReversedTransactionIds); - } else { - updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); - } - - Integer accountType = null; - final SavingsAccountTransactionDTO transactionDTO = new SavingsAccountTransactionDTO(fmt, transactionDate, transactionAmount, - paymentDetail, null, accountType); - UUID refNo = UUID.randomUUID(); - final SavingsAccountTransaction deposit = account.deposit(transactionDTO, savingsAccountTransactionType, backdatedTxnsAllowedTill, - relaxingDaysConfigForPivotDate, refNo.toString()); - final LocalDate postInterestOnDate = null; - final MathContext mc = MathContext.DECIMAL64; - - final LocalDate today = DateUtils.getBusinessLocalDate(); - boolean postReversals = this.configurationDomainService.isReversalTransactionAllowed(); - if (account.isBeforeLastPostingPeriod(transactionDate, backdatedTxnsAllowedTill)) { - account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, - postInterestOnDate, backdatedTxnsAllowedTill, postReversals); - } else { - account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals); - } - - saveTransactionToGenerateTransactionId(deposit); - - if (backdatedTxnsAllowedTill) { - // Update transactions separately - saveUpdatedTransactionsOfSavingsAccount(account.getSavingsAccountTransactionsWithPivotConfig()); - } - - this.savingsAccountRepository.saveAndFlush(account); - - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill); - businessEventNotifierService.notifyPostBusinessEvent(new SavingsDepositBusinessEvent(deposit)); - return deposit; - } - - @Transactional - @Override - public SavingsAccountTransaction handleHold(final SavingsAccount account, BigDecimal amount, LocalDate transactionDate, - Boolean lienAllowed) { - return SavingsAccountTransaction.holdAmount(account, account.office(), null, transactionDate, - Money.of(account.getCurrency(), amount), lienAllowed); - } - - @Override - public SavingsAccountTransaction handleDividendPayout(final SavingsAccount account, final LocalDate transactionDate, - final BigDecimal transactionAmount, final boolean backdatedTxnsAllowedTill) { - final DateTimeFormatter fmt = null; - final PaymentDetail paymentDetail = null; - final boolean isAccountTransfer = false; - final boolean isRegularTransaction = true; - final SavingsAccountTransactionType savingsAccountTransactionType = SavingsAccountTransactionType.DIVIDEND_PAYOUT; - return handleDeposit(account, fmt, transactionDate, transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction, - savingsAccountTransactionType, backdatedTxnsAllowedTill); - } - - private void updateExistingTransactionsDetails(SavingsAccount account, Set existingTransactionIds, - Set existingReversedTransactionIds) { - existingTransactionIds.addAll(account.findExistingTransactionIds()); - existingReversedTransactionIds.addAll(account.findExistingReversedTransactionIds()); - } - - private Long saveTransactionToGenerateTransactionId(final SavingsAccountTransaction transaction) { - this.savingsAccountTransactionRepository.saveAndFlush(transaction); - return transaction.getId(); - } - - private void saveUpdatedTransactionsOfSavingsAccount(final List savingsAccountTransactions) { - this.savingsAccountTransactionRepository.saveAll(savingsAccountTransactions); - } - - private void updateTransactionDetailsWithPivotConfig(final SavingsAccount account, Set existingTransactionIds, - Set existingReversedTransactionIds) { - existingTransactionIds.addAll(account.findCurrentTransactionIdsWithPivotDateConfig()); - existingReversedTransactionIds.addAll(account.findCurrentReversedTransactionIdsWithPivotDateConfig()); - } - - private void postJournalEntries(final SavingsAccount savingsAccount, final Set existingTransactionIds, - final Set existingReversedTransactionIds, boolean isAccountTransfer, final boolean backdatedTxnsAllowedTill) { - - final Map accountingBridgeData = savingsAccount.deriveAccountingBridgeData(savingsAccount.getCurrency().getCode(), - existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill); - this.journalEntryWritePlatformService.createJournalEntriesForSavings(accountingBridgeData); - } - - @Transactional - @Override - public void postJournalEntries(final SavingsAccount account, final Set existingTransactionIds, - final Set existingReversedTransactionIds, final boolean backdatedTxnsAllowedTill) { - - final boolean isAccountTransfer = false; - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill); - } - - @Override - public SavingsAccountTransaction handleReversal(SavingsAccount account, List savingsAccountTransactions, - boolean backdatedTxnsAllowedTill) { - - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - final Long relaxingDaysConfigForPivotDate = this.configurationDomainService.retrieveRelaxingDaysConfigForPivotDate(); - final boolean postReversals = true; - final Set existingTransactionIds = new HashSet<>(); - final Set existingReversedTransactionIds = new HashSet<>(); - - if (backdatedTxnsAllowedTill) { - updateTransactionDetailsWithPivotConfig(account, existingTransactionIds, existingReversedTransactionIds); - } else { - updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); - } - List newTransactions = new ArrayList<>(); - SavingsAccountTransaction reversal = null; - - Set chargePaidBySet = null; - for (SavingsAccountTransaction savingsAccountTransaction : savingsAccountTransactions) { - reversal = SavingsAccountTransaction.reversal(savingsAccountTransaction); - chargePaidBySet = savingsAccountTransaction.getSavingsAccountChargesPaid(); - reversal.getSavingsAccountChargesPaid().addAll(chargePaidBySet); - account.undoTransaction(savingsAccountTransaction); - if (postReversals) { - newTransactions.add(reversal); - } - } - - boolean isInterestTransfer = false; - LocalDate postInterestOnDate = null; - final LocalDate today = DateUtils.getBusinessLocalDate(); - final MathContext mc = new MathContext(15, MoneyHelper.getRoundingMode()); - for (SavingsAccountTransaction savingsAccountTransaction : savingsAccountTransactions) { - if (savingsAccountTransaction.isPostInterestCalculationRequired() - && account.isBeforeLastPostingPeriod(savingsAccountTransaction.getTransactionDate(), backdatedTxnsAllowedTill)) { - - account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, - postInterestOnDate, backdatedTxnsAllowedTill, postReversals); - } else { - account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals); - } - account.validatePivotDateTransaction(savingsAccountTransaction.getTransactionDate(), backdatedTxnsAllowedTill, - relaxingDaysConfigForPivotDate, "savingsaccount"); - account.validateAccountBalanceDoesNotBecomeNegativeMinimal(savingsAccountTransaction.getAmount(), false); - account.activateAccountBasedOnBalance(); - } - this.savingsAccountRepository.save(account); - newTransactions.addAll(account.getSavingsAccountTransactionsWithPivotConfig()); - this.savingsAccountTransactionRepository.saveAll(newTransactions); - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false, backdatedTxnsAllowedTill); - - return reversal; - } -} +/** + * 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 java.math.BigDecimal; +import java.math.MathContext; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService; +import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.event.business.domain.savings.transaction.SavingsDepositBusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.savings.transaction.SavingsWithdrawalBusinessEvent; +import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.organisation.monetary.domain.ApplicationCurrencyRepositoryWrapper; +import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.organisation.monetary.domain.MoneyHelper; +import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail; +import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType; +import org.apache.fineract.portfolio.savings.SavingsTransactionBooleanValues; +import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionDTO; +import org.apache.fineract.portfolio.savings.exception.DepositAccountTransactionNotAllowedException; +import org.apache.fineract.portfolio.savings.service.SavingsAccountDomainService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class SavingsAccountDomainServiceJpa implements SavingsAccountDomainService { + + private final PlatformSecurityContext context; + private final SavingsAccountRepositoryWrapper savingsAccountRepository; + private final SavingsAccountTransactionRepository savingsAccountTransactionRepository; + private final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepositoryWrapper; + private final JournalEntryWritePlatformService journalEntryWritePlatformService; + private final ConfigurationDomainService configurationDomainService; + private final DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository; + private final BusinessEventNotifierService businessEventNotifierService; + + @Autowired + public SavingsAccountDomainServiceJpa(final SavingsAccountRepositoryWrapper savingsAccountRepository, + final SavingsAccountTransactionRepository savingsAccountTransactionRepository, + final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepositoryWrapper, + final JournalEntryWritePlatformService journalEntryWritePlatformService, + final ConfigurationDomainService configurationDomainService, final PlatformSecurityContext context, + final DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository, + final BusinessEventNotifierService businessEventNotifierService) { + this.savingsAccountRepository = savingsAccountRepository; + this.savingsAccountTransactionRepository = savingsAccountTransactionRepository; + this.applicationCurrencyRepositoryWrapper = applicationCurrencyRepositoryWrapper; + this.journalEntryWritePlatformService = journalEntryWritePlatformService; + this.configurationDomainService = configurationDomainService; + this.context = context; + this.depositAccountOnHoldTransactionRepository = depositAccountOnHoldTransactionRepository; + this.businessEventNotifierService = businessEventNotifierService; + } + + @Transactional + @Override + public SavingsAccountTransaction handleWithdrawal(final SavingsAccount account, final DateTimeFormatter fmt, + final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail, + final SavingsTransactionBooleanValues transactionBooleanValues, final boolean backdatedTxnsAllowedTill) { + context.authenticatedUser(); + account.validateForAccountBlock(); + account.validateForDebitBlock(); + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Long relaxingDaysConfigForPivotDate = this.configurationDomainService.retrieveRelaxingDaysConfigForPivotDate(); + final boolean postReversals = this.configurationDomainService.isReversalTransactionAllowed(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + if (transactionBooleanValues.isRegularTransaction() && !account.allowWithdrawal()) { + throw new DepositAccountTransactionNotAllowedException(account.getId(), "withdraw", account.depositAccountType()); + } + final Set existingTransactionIds = new HashSet<>(); + final LocalDate postInterestOnDate = null; + final Set existingReversedTransactionIds = new HashSet<>(); + + if (backdatedTxnsAllowedTill) { + updateTransactionDetailsWithPivotConfig(account, existingTransactionIds, existingReversedTransactionIds); + } else { + updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + } + + Integer accountType = null; + final SavingsAccountTransactionDTO transactionDTO = new SavingsAccountTransactionDTO(fmt, transactionDate, transactionAmount, + paymentDetail, null, accountType); + UUID refNo = UUID.randomUUID(); + final SavingsAccountTransaction withdrawal = account.withdraw(transactionDTO, transactionBooleanValues.isApplyWithdrawFee(), + backdatedTxnsAllowedTill, relaxingDaysConfigForPivotDate, refNo.toString()); + final MathContext mc = MathContext.DECIMAL64; + + final LocalDate today = DateUtils.getBusinessLocalDate(); + + if (account.isBeforeLastPostingPeriod(transactionDate, backdatedTxnsAllowedTill)) { + account.postInterest(mc, today, transactionBooleanValues.isInterestTransfer(), isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals); + } else { + account.calculateInterestUsing(mc, today, transactionBooleanValues.isInterestTransfer(), + isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, + postReversals); + } + + List depositAccountOnHoldTransactions = null; + if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { + depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository + .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); + } + + account.validateAccountBalanceDoesNotBecomeNegative(transactionAmount, transactionBooleanValues.isExceptionForBalanceCheck(), + depositAccountOnHoldTransactions, backdatedTxnsAllowedTill); + + saveTransactionToGenerateTransactionId(withdrawal); + if (backdatedTxnsAllowedTill) { + // Update transactions separately + saveUpdatedTransactionsOfSavingsAccount(account.getSavingsAccountTransactionsWithPivotConfig()); + } + this.savingsAccountRepository.save(account); + + if (!transactionBooleanValues.isSkipAccountingPostings()) { + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, + transactionBooleanValues.isAccountTransfer(), backdatedTxnsAllowedTill); + } + + businessEventNotifierService.notifyPostBusinessEvent(new SavingsWithdrawalBusinessEvent(withdrawal)); + return withdrawal; + } + + @Transactional + @Override + public SavingsAccountTransaction handleDeposit(final SavingsAccount account, final DateTimeFormatter fmt, + final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail, + final boolean isAccountTransfer, final boolean isRegularTransaction, final boolean backdatedTxnsAllowedTill) { + final SavingsAccountTransactionType savingsAccountTransactionType = SavingsAccountTransactionType.DEPOSIT; + return handleDeposit(account, fmt, transactionDate, transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction, + savingsAccountTransactionType, backdatedTxnsAllowedTill, false); + } + + @Override + public SavingsAccountTransaction handleDeposit(final SavingsAccount account, final DateTimeFormatter fmt, + final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail, + final boolean isAccountTransfer, final boolean isRegularTransaction, final boolean backdatedTxnsAllowedTill, + final boolean skipAccountingPostings) { + final SavingsAccountTransactionType savingsAccountTransactionType = SavingsAccountTransactionType.DEPOSIT; + return handleDeposit(account, fmt, transactionDate, transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction, + savingsAccountTransactionType, backdatedTxnsAllowedTill, skipAccountingPostings); + } + + private SavingsAccountTransaction handleDeposit(final SavingsAccount account, final DateTimeFormatter fmt, + final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail, + final boolean isAccountTransfer, final boolean isRegularTransaction, + final SavingsAccountTransactionType savingsAccountTransactionType, final boolean backdatedTxnsAllowedTill) { + return handleDeposit(account, fmt, transactionDate, transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction, + savingsAccountTransactionType, backdatedTxnsAllowedTill, false); + } + + private SavingsAccountTransaction handleDeposit(final SavingsAccount account, final DateTimeFormatter fmt, + final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail, + final boolean isAccountTransfer, final boolean isRegularTransaction, + final SavingsAccountTransactionType savingsAccountTransactionType, final boolean backdatedTxnsAllowedTill, + final boolean skipAccountingPostings) { + context.authenticatedUser(); + account.validateForAccountBlock(); + account.validateForCreditBlock(); + + // Global configurations + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + final Long relaxingDaysConfigForPivotDate = this.configurationDomainService.retrieveRelaxingDaysConfigForPivotDate(); + if (isRegularTransaction && !account.allowDeposit()) { + throw new DepositAccountTransactionNotAllowedException(account.getId(), "deposit", account.depositAccountType()); + } + boolean isInterestTransfer = false; + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + + if (backdatedTxnsAllowedTill) { + updateTransactionDetailsWithPivotConfig(account, existingTransactionIds, existingReversedTransactionIds); + } else { + updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + } + + Integer accountType = null; + final SavingsAccountTransactionDTO transactionDTO = new SavingsAccountTransactionDTO(fmt, transactionDate, transactionAmount, + paymentDetail, null, accountType); + UUID refNo = UUID.randomUUID(); + final SavingsAccountTransaction deposit = account.deposit(transactionDTO, savingsAccountTransactionType, backdatedTxnsAllowedTill, + relaxingDaysConfigForPivotDate, refNo.toString()); + final LocalDate postInterestOnDate = null; + final MathContext mc = MathContext.DECIMAL64; + + final LocalDate today = DateUtils.getBusinessLocalDate(); + boolean postReversals = this.configurationDomainService.isReversalTransactionAllowed(); + if (account.isBeforeLastPostingPeriod(transactionDate, backdatedTxnsAllowedTill)) { + account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, + postInterestOnDate, backdatedTxnsAllowedTill, postReversals); + } else { + account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals); + } + + saveTransactionToGenerateTransactionId(deposit); + + if (backdatedTxnsAllowedTill) { + // Update transactions separately + saveUpdatedTransactionsOfSavingsAccount(account.getSavingsAccountTransactionsWithPivotConfig()); + } + + this.savingsAccountRepository.saveAndFlush(account); + + if (!skipAccountingPostings) { + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill); + } + businessEventNotifierService.notifyPostBusinessEvent(new SavingsDepositBusinessEvent(deposit)); + return deposit; + } + + @Transactional + @Override + public SavingsAccountTransaction handleHold(final SavingsAccount account, BigDecimal amount, LocalDate transactionDate, + Boolean lienAllowed) { + return SavingsAccountTransaction.holdAmount(account, account.office(), null, transactionDate, + Money.of(account.getCurrency(), amount), lienAllowed); + } + + @Override + public SavingsAccountTransaction handleDividendPayout(final SavingsAccount account, final LocalDate transactionDate, + final BigDecimal transactionAmount, final boolean backdatedTxnsAllowedTill) { + final DateTimeFormatter fmt = null; + final PaymentDetail paymentDetail = null; + final boolean isAccountTransfer = false; + final boolean isRegularTransaction = true; + final SavingsAccountTransactionType savingsAccountTransactionType = SavingsAccountTransactionType.DIVIDEND_PAYOUT; + return handleDeposit(account, fmt, transactionDate, transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction, + savingsAccountTransactionType, backdatedTxnsAllowedTill); + } + + private void updateExistingTransactionsDetails(SavingsAccount account, Set existingTransactionIds, + Set existingReversedTransactionIds) { + existingTransactionIds.addAll(account.findExistingTransactionIds()); + existingReversedTransactionIds.addAll(account.findExistingReversedTransactionIds()); + } + + private Long saveTransactionToGenerateTransactionId(final SavingsAccountTransaction transaction) { + this.savingsAccountTransactionRepository.saveAndFlush(transaction); + return transaction.getId(); + } + + private void saveUpdatedTransactionsOfSavingsAccount(final List savingsAccountTransactions) { + this.savingsAccountTransactionRepository.saveAll(savingsAccountTransactions); + } + + private void updateTransactionDetailsWithPivotConfig(final SavingsAccount account, Set existingTransactionIds, + Set existingReversedTransactionIds) { + existingTransactionIds.addAll(account.findCurrentTransactionIdsWithPivotDateConfig()); + existingReversedTransactionIds.addAll(account.findCurrentReversedTransactionIdsWithPivotDateConfig()); + } + + private void postJournalEntries(final SavingsAccount savingsAccount, final Set existingTransactionIds, + final Set existingReversedTransactionIds, boolean isAccountTransfer, final boolean backdatedTxnsAllowedTill) { + + final Map accountingBridgeData = savingsAccount.deriveAccountingBridgeData(savingsAccount.getCurrency().getCode(), + existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill); + this.journalEntryWritePlatformService.createJournalEntriesForSavings(accountingBridgeData); + } + + @Transactional + @Override + public void postJournalEntries(final SavingsAccount account, final Set existingTransactionIds, + final Set existingReversedTransactionIds, final boolean backdatedTxnsAllowedTill) { + + final boolean isAccountTransfer = false; + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill); + } + + @Override + public SavingsAccountTransaction handleReversal(SavingsAccount account, List savingsAccountTransactions, + boolean backdatedTxnsAllowedTill) { + + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + final Long relaxingDaysConfigForPivotDate = this.configurationDomainService.retrieveRelaxingDaysConfigForPivotDate(); + final boolean postReversals = true; + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + + if (backdatedTxnsAllowedTill) { + updateTransactionDetailsWithPivotConfig(account, existingTransactionIds, existingReversedTransactionIds); + } else { + updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + } + List newTransactions = new ArrayList<>(); + SavingsAccountTransaction reversal = null; + + Set chargePaidBySet = null; + for (SavingsAccountTransaction savingsAccountTransaction : savingsAccountTransactions) { + reversal = SavingsAccountTransaction.reversal(savingsAccountTransaction); + chargePaidBySet = savingsAccountTransaction.getSavingsAccountChargesPaid(); + reversal.getSavingsAccountChargesPaid().addAll(chargePaidBySet); + account.undoTransaction(savingsAccountTransaction); + if (postReversals) { + newTransactions.add(reversal); + } + } + + boolean isInterestTransfer = false; + LocalDate postInterestOnDate = null; + final LocalDate today = DateUtils.getBusinessLocalDate(); + final MathContext mc = new MathContext(15, MoneyHelper.getRoundingMode()); + for (SavingsAccountTransaction savingsAccountTransaction : savingsAccountTransactions) { + if (savingsAccountTransaction.isPostInterestCalculationRequired() + && account.isBeforeLastPostingPeriod(savingsAccountTransaction.getTransactionDate(), backdatedTxnsAllowedTill)) { + + account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, + postInterestOnDate, backdatedTxnsAllowedTill, postReversals); + } else { + account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals); + } + account.validatePivotDateTransaction(savingsAccountTransaction.getTransactionDate(), backdatedTxnsAllowedTill, + relaxingDaysConfigForPivotDate, "savingsaccount"); + account.validateAccountBalanceDoesNotBecomeNegativeMinimal(savingsAccountTransaction.getAmount(), false); + account.activateAccountBasedOnBalance(); + } + this.savingsAccountRepository.save(account); + newTransactions.addAll(account.getSavingsAccountTransactionsWithPivotConfig()); + this.savingsAccountTransactionRepository.saveAll(newTransactions); + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false, backdatedTxnsAllowedTill); + + return reversal; + } +} diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/SavingsTransactionBooleanValues.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/SavingsTransactionBooleanValues.java index 2cef4adfc..f3f8a79cb 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/SavingsTransactionBooleanValues.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/SavingsTransactionBooleanValues.java @@ -1,59 +1,70 @@ -/** - * 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; - -public class SavingsTransactionBooleanValues { - - private final boolean isAccountTransfer; - private final boolean isRegularTransaction; - private final boolean isApplyWithdrawFee; - private final boolean isInterestTransfer; - private final boolean isExceptionForBalanceCheck; - - public SavingsTransactionBooleanValues(final boolean isAccountTransfer, final boolean isRegularTransaction, - final boolean isApplyWithdrawFee, final boolean isInterestTransfer, final boolean isExceptionForBalanceCheck) { - - this.isAccountTransfer = isAccountTransfer; - this.isRegularTransaction = isRegularTransaction; - this.isApplyWithdrawFee = isApplyWithdrawFee; - this.isInterestTransfer = isInterestTransfer; - this.isExceptionForBalanceCheck = isExceptionForBalanceCheck; - } - - public boolean isAccountTransfer() { - return this.isAccountTransfer; - } - - public boolean isRegularTransaction() { - return this.isRegularTransaction; - } - - public boolean isApplyWithdrawFee() { - return this.isApplyWithdrawFee; - } - - public boolean isInterestTransfer() { - return this.isInterestTransfer; - } - - public boolean isExceptionForBalanceCheck() { - return this.isExceptionForBalanceCheck; - } - -} +/** + * 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; + +public class SavingsTransactionBooleanValues { + + private final boolean isAccountTransfer; + private final boolean isRegularTransaction; + private final boolean isApplyWithdrawFee; + private final boolean isInterestTransfer; + private final boolean isExceptionForBalanceCheck; + private final boolean skipAccountingPostings; + + public SavingsTransactionBooleanValues(final boolean isAccountTransfer, final boolean isRegularTransaction, + final boolean isApplyWithdrawFee, final boolean isInterestTransfer, final boolean isExceptionForBalanceCheck) { + this(isAccountTransfer, isRegularTransaction, isApplyWithdrawFee, isInterestTransfer, isExceptionForBalanceCheck, false); + } + + public SavingsTransactionBooleanValues(final boolean isAccountTransfer, final boolean isRegularTransaction, + final boolean isApplyWithdrawFee, final boolean isInterestTransfer, final boolean isExceptionForBalanceCheck, + final boolean skipAccountingPostings) { + this.isAccountTransfer = isAccountTransfer; + this.isRegularTransaction = isRegularTransaction; + this.isApplyWithdrawFee = isApplyWithdrawFee; + this.isInterestTransfer = isInterestTransfer; + this.isExceptionForBalanceCheck = isExceptionForBalanceCheck; + this.skipAccountingPostings = skipAccountingPostings; + } + + public boolean isAccountTransfer() { + return this.isAccountTransfer; + } + + public boolean isRegularTransaction() { + return this.isRegularTransaction; + } + + public boolean isApplyWithdrawFee() { + return this.isApplyWithdrawFee; + } + + public boolean isInterestTransfer() { + return this.isInterestTransfer; + } + + public boolean isExceptionForBalanceCheck() { + return this.isExceptionForBalanceCheck; + } + + public boolean isSkipAccountingPostings() { + return this.skipAccountingPostings; + } + +} diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountDomainService.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountDomainService.java index 325c9dd8a..bdd17f5b2 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountDomainService.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountDomainService.java @@ -1,51 +1,55 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.fineract.portfolio.savings.service; - -import java.math.BigDecimal; -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; -import java.util.List; -import java.util.Set; -import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail; -import org.apache.fineract.portfolio.savings.SavingsTransactionBooleanValues; -import org.apache.fineract.portfolio.savings.domain.SavingsAccount; -import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction; - -public interface SavingsAccountDomainService { - - SavingsAccountTransaction handleWithdrawal(SavingsAccount account, DateTimeFormatter fmt, LocalDate transactionDate, - BigDecimal transactionAmount, PaymentDetail paymentDetail, SavingsTransactionBooleanValues transactionBooleanValues, - boolean backdatedTxnsAllowedTill); - - SavingsAccountTransaction handleDeposit(SavingsAccount account, DateTimeFormatter fmt, LocalDate transactionDate, - BigDecimal transactionAmount, PaymentDetail paymentDetail, boolean isAccountTransfer, boolean isRegularTransaction, - boolean backdatedTxnsAllowedTill); - - void postJournalEntries(SavingsAccount savingsAccount, Set existingTransactionIds, Set existingReversedTransactionIds, - boolean backdatedTxnsAllowedTill); - - SavingsAccountTransaction handleDividendPayout(SavingsAccount account, LocalDate transactionDate, BigDecimal transactionAmount, - boolean backdatedTxnsAllowedTill); - - SavingsAccountTransaction handleReversal(SavingsAccount account, List savingsAccountTransactions, - boolean backdatedTxnsAllowedTill); - - SavingsAccountTransaction handleHold(SavingsAccount account, BigDecimal amount, LocalDate transactionDate, Boolean lienAllowed); -} +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.savings.service; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Set; +import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail; +import org.apache.fineract.portfolio.savings.SavingsTransactionBooleanValues; +import org.apache.fineract.portfolio.savings.domain.SavingsAccount; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction; + +public interface SavingsAccountDomainService { + + SavingsAccountTransaction handleWithdrawal(SavingsAccount account, DateTimeFormatter fmt, LocalDate transactionDate, + BigDecimal transactionAmount, PaymentDetail paymentDetail, SavingsTransactionBooleanValues transactionBooleanValues, + boolean backdatedTxnsAllowedTill); + + SavingsAccountTransaction handleDeposit(SavingsAccount account, DateTimeFormatter fmt, LocalDate transactionDate, + BigDecimal transactionAmount, PaymentDetail paymentDetail, boolean isAccountTransfer, boolean isRegularTransaction, + boolean backdatedTxnsAllowedTill); + + SavingsAccountTransaction handleDeposit(SavingsAccount account, DateTimeFormatter fmt, LocalDate transactionDate, + BigDecimal transactionAmount, PaymentDetail paymentDetail, boolean isAccountTransfer, boolean isRegularTransaction, + boolean backdatedTxnsAllowedTill, boolean skipAccountingPostings); + + void postJournalEntries(SavingsAccount savingsAccount, Set existingTransactionIds, Set existingReversedTransactionIds, + boolean backdatedTxnsAllowedTill); + + SavingsAccountTransaction handleDividendPayout(SavingsAccount account, LocalDate transactionDate, BigDecimal transactionAmount, + boolean backdatedTxnsAllowedTill); + + SavingsAccountTransaction handleReversal(SavingsAccount account, List savingsAccountTransactions, + boolean backdatedTxnsAllowedTill); + + SavingsAccountTransaction handleHold(SavingsAccount account, BigDecimal amount, LocalDate transactionDate, Boolean lienAllowed); +} From ee915e395aa704f709125424b9d15f0474adf608 Mon Sep 17 00:00:00 2001 From: Jaanu423 Date: Wed, 27 May 2026 11:16:52 +0530 Subject: [PATCH 05/10] feat: Denomination Master backend - master, audit, soft delete, txn breakdown - m_denomination_master CRUD API with duplicate (denomination+kind) validation, soft delete + restore, and a full audit trail (m_denomination_master_audit logs CREATE/UPDATE/ACTIVATE/DEACTIVATE/ DELETE/RESTORE with snapshot, changes, user and timestamp) - m_savings_txn_denomination: persist per-transaction cash denomination breakdown on savings deposit; allow denominationBreakdown request param - Liquibase 0012_denomination_master registered in tenant changelog Co-Authored-By: Claude Opus 4.7 (1M context) --- .../api/DenominationMasterApiResource.java | 232 + .../domain/DenominationMaster.java | 62 + .../domain/DenominationMasterAudit.java | 83 + .../DenominationMasterAuditRepository.java | 17 + .../domain/DenominationMasterRepository.java | 15 + .../domain/SavingsTxnDenomination.java | 81 + .../SavingsTxnDenominationRepository.java | 15 + ...WritePlatformServiceJpaRepositoryImpl.java | 3947 +++++++++-------- .../savings/starter/SavingsConfiguration.java | 6 +- .../db/changelog/tenant/changelog-tenant.xml | 1 + .../tenant/parts/0012_denomination_master.xml | 180 + .../savings/data/SavingsAccountConstant.java | 165 +- 12 files changed, 2765 insertions(+), 2039 deletions(-) create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/api/DenominationMasterApiResource.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/domain/DenominationMaster.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/domain/DenominationMasterAudit.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/domain/DenominationMasterAuditRepository.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/domain/DenominationMasterRepository.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/domain/SavingsTxnDenomination.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/domain/SavingsTxnDenominationRepository.java create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0012_denomination_master.xml diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/api/DenominationMasterApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/api/DenominationMasterApiResource.java new file mode 100644 index 000000000..36166bffa --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/api/DenominationMasterApiResource.java @@ -0,0 +1,232 @@ +package org.apache.fineract.portfolio.denominationmaster.api; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.portfolio.denominationmaster.domain.DenominationMaster; +import org.apache.fineract.portfolio.denominationmaster.domain.DenominationMasterAudit; +import org.apache.fineract.portfolio.denominationmaster.domain.DenominationMasterAuditRepository; +import org.apache.fineract.portfolio.denominationmaster.domain.DenominationMasterRepository; +import org.apache.fineract.useradministration.domain.AppUser; +import org.springframework.stereotype.Component; + +@Path("/v1/denominationmaster") +@Component +@RequiredArgsConstructor +@Consumes({ MediaType.APPLICATION_JSON }) +@Produces({ MediaType.APPLICATION_JSON }) +public class DenominationMasterApiResource { + + private final DenominationMasterRepository repository; + private final DenominationMasterAuditRepository auditRepository; + private final PlatformSecurityContext context; + private final Gson gson = new Gson(); + + @GET + public String getAll() { + List list = repository.findByDeletedFalseOrderByKindDescDenominationDesc(); + return gson.toJson(list.stream().map(this::toMap).collect(Collectors.toList())); + } + + @GET + @Path("active") + public String getActive() { + List list = repository.findByIsActiveTrueAndDeletedFalseOrderByKindDescDenominationDesc(); + return gson.toJson(list.stream().map(this::toMap).collect(Collectors.toList())); + } + + @GET + @Path("deleted") + public String getDeleted() { + List list = repository.findByDeletedTrueOrderByKindDescDenominationDesc(); + return gson.toJson(list.stream().map(this::toMap).collect(Collectors.toList())); + } + + @GET + @Path("audit") + public String getAllAudit() { + List list = auditRepository.findAllByOrderByPerformedOnDesc(); + return gson.toJson(list.stream().map(this::auditToMap).collect(Collectors.toList())); + } + + @GET + @Path("{id}/audit") + public String getAuditForDenomination(@PathParam("id") Long id) { + List list = auditRepository.findByDenominationIdOrderByPerformedOnDesc(id); + return gson.toJson(list.stream().map(this::auditToMap).collect(Collectors.toList())); + } + + @POST + public String create(String body) { + JsonObject json = JsonParser.parseString(body).getAsJsonObject(); + final int denomination = json.get("denomination").getAsInt(); + final String kind = json.get("kind").getAsString(); + if (repository.existsByDenominationAndKindAndDeletedFalse(denomination, kind)) { + throw new PlatformDataIntegrityException("error.msg.denomination.duplicate", + "A " + kind + " of denomination " + denomination + " already exists.", "denomination", denomination); + } + DenominationMaster dm = DenominationMaster.create(denomination, kind, + json.has("description") ? json.get("description").getAsString() : null, + json.has("isActive") ? json.get("isActive").getAsBoolean() : true, + json.has("introducedOn") ? LocalDate.parse(json.get("introducedOn").getAsString()) : null, null); + DenominationMaster saved = repository.save(dm); + writeAudit("CREATE", saved, null); + JsonObject result = new JsonObject(); + result.addProperty("resourceId", saved.getId()); + return gson.toJson(result); + } + + @PUT + @Path("{id}") + public String update(@PathParam("id") Long id, @QueryParam("command") String command, String body) { + DenominationMaster dm = repository.findById(id).orElseThrow(() -> new RuntimeException("Denomination not found: " + id)); + + if ("activate".equals(command)) { + dm.setIsActive(true); + dm.setDiscontinuedOn(null); + repository.save(dm); + writeAudit("ACTIVATE", dm, null); + } else if ("deactivate".equals(command)) { + dm.setIsActive(false); + dm.setDiscontinuedOn(LocalDate.now()); + repository.save(dm); + writeAudit("DEACTIVATE", dm, null); + } else if ("restore".equals(command)) { + if (repository.existsByDenominationAndKindAndDeletedFalseAndIdNot(dm.getDenomination(), dm.getKind(), id)) { + throw new PlatformDataIntegrityException("error.msg.denomination.duplicate.restore", + "Cannot restore: a " + dm.getKind() + " of denomination " + dm.getDenomination() + " already exists.", + "denomination", dm.getDenomination()); + } + dm.setDeleted(false); + repository.save(dm); + writeAudit("RESTORE", dm, null); + } else { + JsonObject json = JsonParser.parseString(body).getAsJsonObject(); + final int effectiveDenomination = json.has("denomination") ? json.get("denomination").getAsInt() : dm.getDenomination(); + final String effectiveKind = json.has("kind") ? json.get("kind").getAsString() : dm.getKind(); + if (repository.existsByDenominationAndKindAndDeletedFalseAndIdNot(effectiveDenomination, effectiveKind, id)) { + throw new PlatformDataIntegrityException("error.msg.denomination.duplicate", + "A " + effectiveKind + " of denomination " + effectiveDenomination + " already exists.", "denomination", + effectiveDenomination); + } + List changes = new ArrayList<>(); + if (json.has("denomination")) { + int newVal = json.get("denomination").getAsInt(); + if (!java.util.Objects.equals(dm.getDenomination(), newVal)) { + changes.add("denomination: " + dm.getDenomination() + " -> " + newVal); + } + dm.setDenomination(newVal); + } + if (json.has("kind")) { + String newVal = json.get("kind").getAsString(); + if (!java.util.Objects.equals(dm.getKind(), newVal)) { + changes.add("kind: " + dm.getKind() + " -> " + newVal); + } + dm.setKind(newVal); + } + if (json.has("description")) { + String newVal = json.get("description").getAsString(); + if (!java.util.Objects.equals(dm.getDescription(), newVal)) { + changes.add("description: " + dm.getDescription() + " -> " + newVal); + } + dm.setDescription(newVal); + } + if (json.has("isActive")) { + boolean newVal = json.get("isActive").getAsBoolean(); + if (!java.util.Objects.equals(dm.getIsActive(), newVal)) { + changes.add("isActive: " + dm.getIsActive() + " -> " + newVal); + } + dm.setIsActive(newVal); + } + if (json.has("introducedOn")) { + LocalDate newVal = LocalDate.parse(json.get("introducedOn").getAsString()); + if (!java.util.Objects.equals(dm.getIntroducedOn(), newVal)) { + changes.add("introducedOn: " + dm.getIntroducedOn() + " -> " + newVal); + } + dm.setIntroducedOn(newVal); + } + repository.save(dm); + writeAudit("UPDATE", dm, changes.isEmpty() ? null : String.join("; ", changes)); + } + JsonObject result = new JsonObject(); + result.addProperty("resourceId", id); + return gson.toJson(result); + } + + @DELETE + @Path("{id}") + public String delete(@PathParam("id") Long id) { + DenominationMaster dm = repository.findById(id).orElseThrow(() -> new RuntimeException("Denomination not found: " + id)); + // soft delete: flag the row instead of removing it, so it can be restored and history is preserved + dm.setDeleted(true); + dm.setIsActive(false); + repository.save(dm); + writeAudit("DELETE", dm, null); + JsonObject result = new JsonObject(); + result.addProperty("resourceId", id); + return gson.toJson(result); + } + + private void writeAudit(final String action, final DenominationMaster dm, final String changes) { + Long userId = null; + String username = null; + try { + AppUser user = this.context.authenticatedUser(); + if (user != null) { + userId = user.getId(); + username = user.getUsername(); + } + } catch (Exception e) { + // audit should never block the main operation if the user cannot be resolved + } + auditRepository.save(DenominationMasterAudit.of(action, dm, changes, userId, username)); + } + + private java.util.Map toMap(DenominationMaster dm) { + java.util.Map map = new java.util.LinkedHashMap<>(); + map.put("id", dm.getId()); + map.put("denomination", dm.getDenomination()); + map.put("kind", dm.getKind()); + map.put("description", dm.getDescription()); + map.put("isActive", dm.getIsActive()); + map.put("introducedOn", dm.getIntroducedOn() != null ? dm.getIntroducedOn().toString() : null); + map.put("discontinuedOn", dm.getDiscontinuedOn() != null ? dm.getDiscontinuedOn().toString() : null); + map.put("deleted", dm.getDeleted()); + return map; + } + + private java.util.Map auditToMap(DenominationMasterAudit a) { + java.util.Map map = new java.util.LinkedHashMap<>(); + map.put("id", a.getId()); + map.put("denominationId", a.getDenominationId()); + map.put("action", a.getAction()); + map.put("denomination", a.getDenomination()); + map.put("kind", a.getKind()); + map.put("description", a.getDescription()); + map.put("isActive", a.getIsActive()); + map.put("introducedOn", a.getIntroducedOn() != null ? a.getIntroducedOn().toString() : null); + map.put("discontinuedOn", a.getDiscontinuedOn() != null ? a.getDiscontinuedOn().toString() : null); + map.put("changes", a.getChanges()); + map.put("performedBy", a.getPerformedBy()); + map.put("performedByUsername", a.getPerformedByUsername()); + map.put("performedOn", a.getPerformedOn() != null ? a.getPerformedOn().toString() : null); + return map; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/domain/DenominationMaster.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/domain/DenominationMaster.java new file mode 100644 index 000000000..fcd83ea30 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/domain/DenominationMaster.java @@ -0,0 +1,62 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. + */ +package org.apache.fineract.portfolio.denominationmaster.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.time.LocalDate; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Table(name = "m_denomination_master") +@Getter +@Setter +@NoArgsConstructor +public class DenominationMaster { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "denomination", nullable = false) + private Integer denomination; + + @Column(name = "kind", nullable = false, length = 10) + private String kind; + + @Column(name = "description", length = 100) + private String description; + + @Column(name = "is_active", nullable = false) + private Boolean isActive = true; + + @Column(name = "introduced_on") + private LocalDate introducedOn; + + @Column(name = "discontinued_on") + private LocalDate discontinuedOn; + + @Column(name = "deleted", nullable = false) + private Boolean deleted = false; + + public static DenominationMaster create(Integer denomination, String kind, String description, + Boolean isActive, LocalDate introducedOn, LocalDate discontinuedOn) { + DenominationMaster dm = new DenominationMaster(); + dm.denomination = denomination; + dm.kind = kind; + dm.description = description; + dm.isActive = isActive != null ? isActive : true; + dm.introducedOn = introducedOn; + dm.discontinuedOn = discontinuedOn; + dm.deleted = false; + return dm; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/domain/DenominationMasterAudit.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/domain/DenominationMasterAudit.java new file mode 100644 index 000000000..1e9d05c1a --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/domain/DenominationMasterAudit.java @@ -0,0 +1,83 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. + */ +package org.apache.fineract.portfolio.denominationmaster.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.time.LocalDate; +import java.time.LocalDateTime; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Table(name = "m_denomination_master_audit") +@Getter +@Setter +@NoArgsConstructor +public class DenominationMasterAudit { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "denomination_id") + private Long denominationId; + + @Column(name = "action", nullable = false, length = 20) + private String action; + + @Column(name = "denomination") + private Integer denomination; + + @Column(name = "kind", length = 10) + private String kind; + + @Column(name = "description", length = 100) + private String description; + + @Column(name = "is_active") + private Boolean isActive; + + @Column(name = "introduced_on") + private LocalDate introducedOn; + + @Column(name = "discontinued_on") + private LocalDate discontinuedOn; + + @Column(name = "changes", length = 1000) + private String changes; + + @Column(name = "performed_by") + private Long performedBy; + + @Column(name = "performed_by_username", length = 100) + private String performedByUsername; + + @Column(name = "performed_on", nullable = false) + private LocalDateTime performedOn; + + public static DenominationMasterAudit of(final String action, final DenominationMaster dm, final String changes, + final Long performedBy, final String performedByUsername) { + final DenominationMasterAudit a = new DenominationMasterAudit(); + a.action = action; + a.denominationId = dm.getId(); + a.denomination = dm.getDenomination(); + a.kind = dm.getKind(); + a.description = dm.getDescription(); + a.isActive = dm.getIsActive(); + a.introducedOn = dm.getIntroducedOn(); + a.discontinuedOn = dm.getDiscontinuedOn(); + a.changes = changes; + a.performedBy = performedBy; + a.performedByUsername = performedByUsername; + a.performedOn = LocalDateTime.now(); + return a; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/domain/DenominationMasterAuditRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/domain/DenominationMasterAuditRepository.java new file mode 100644 index 000000000..a66dcbc32 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/domain/DenominationMasterAuditRepository.java @@ -0,0 +1,17 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. + */ +package org.apache.fineract.portfolio.denominationmaster.domain; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface DenominationMasterAuditRepository extends JpaRepository { + + List findByDenominationIdOrderByPerformedOnDesc(Long denominationId); + + List findAllByOrderByPerformedOnDesc(); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/domain/DenominationMasterRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/domain/DenominationMasterRepository.java new file mode 100644 index 000000000..1b93d2024 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/domain/DenominationMasterRepository.java @@ -0,0 +1,15 @@ +package org.apache.fineract.portfolio.denominationmaster.domain; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface DenominationMasterRepository extends JpaRepository { + List findByDeletedFalseOrderByKindDescDenominationDesc(); + List findByIsActiveTrueAndDeletedFalseOrderByKindDescDenominationDesc(); + List findByDeletedTrueOrderByKindDescDenominationDesc(); + + boolean existsByDenominationAndKindAndDeletedFalse(Integer denomination, String kind); + boolean existsByDenominationAndKindAndDeletedFalseAndIdNot(Integer denomination, String kind, Long id); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/domain/SavingsTxnDenomination.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/domain/SavingsTxnDenomination.java new file mode 100644 index 000000000..8db21d345 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/domain/SavingsTxnDenomination.java @@ -0,0 +1,81 @@ +/** + * 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.denominationmaster.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Table(name = "m_savings_txn_denomination") +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SavingsTxnDenomination { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "txn_id", nullable = false) + private Long txnId; + + @Column(name = "savings_account_id", nullable = false) + private Long savingsAccountId; + + @Column(name = "account_no", length = 50) + private String accountNo; + + @Column(name = "client_id") + private Long clientId; + + @Column(name = "cif_no", length = 50) + private String cifNo; + + @Column(name = "denomination_id", nullable = false) + private Long denominationId; + + @Column(name = "denomination_value", nullable = false) + private Integer denominationValue; + + @Column(name = "count", nullable = false) + private Integer count; + + @Column(name = "total_amount", nullable = false, precision = 19, scale = 6) + private BigDecimal totalAmount; + + @Column(name = "txn_date") + private LocalDate txnDate; + + @Column(name = "created_at") + private LocalDateTime createdAt; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/domain/SavingsTxnDenominationRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/domain/SavingsTxnDenominationRepository.java new file mode 100644 index 000000000..b8a14a0aa --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/domain/SavingsTxnDenominationRepository.java @@ -0,0 +1,15 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. + */ +package org.apache.fineract.portfolio.denominationmaster.domain; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface SavingsTxnDenominationRepository extends JpaRepository { + List findByTxnId(Long txnId); + List findBySavingsAccountId(Long savingsAccountId); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java index 53248d7d2..7edfbf915 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java @@ -1,1955 +1,1992 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.fineract.portfolio.savings.service; - -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.SAVINGS_ACCOUNT_CHARGE_RESOURCE_NAME; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.amountParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.chargeIdParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.dueAsOfDateParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.lienAllowedParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.transactionAmountParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.transactionDateParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.withHoldTaxParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.withdrawBalanceParamName; - -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import io.github.resilience4j.retry.annotation.Retry; -import java.math.BigDecimal; -import java.math.MathContext; -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -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 java.util.UUID; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService; -import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; -import org.apache.fineract.infrastructure.core.api.JsonCommand; -import org.apache.fineract.infrastructure.core.data.ApiParameterError; -import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; -import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; -import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; -import org.apache.fineract.infrastructure.core.exception.ErrorHandler; -import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException; -import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; -import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; -import org.apache.fineract.infrastructure.core.exception.PlatformServiceUnavailableException; -import org.apache.fineract.infrastructure.core.service.DateUtils; -import org.apache.fineract.infrastructure.core.service.MathUtil; -import org.apache.fineract.infrastructure.dataqueries.data.EntityTables; -import org.apache.fineract.infrastructure.dataqueries.data.StatusEnum; -import org.apache.fineract.infrastructure.dataqueries.service.EntityDatatableChecksWritePlatformService; -import org.apache.fineract.infrastructure.event.business.domain.savings.SavingsActivateBusinessEvent; -import org.apache.fineract.infrastructure.event.business.domain.savings.SavingsCloseBusinessEvent; -import org.apache.fineract.infrastructure.event.business.domain.savings.SavingsPostInterestBusinessEvent; -import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; -import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; -import org.apache.fineract.organisation.holiday.domain.HolidayRepositoryWrapper; -import org.apache.fineract.organisation.monetary.domain.Money; -import org.apache.fineract.organisation.monetary.domain.MoneyHelper; -import org.apache.fineract.organisation.office.domain.Office; -import org.apache.fineract.organisation.staff.domain.Staff; -import org.apache.fineract.organisation.staff.domain.StaffRepositoryWrapper; -import org.apache.fineract.organisation.workingdays.domain.WorkingDaysRepositoryWrapper; -import org.apache.fineract.portfolio.account.PortfolioAccountType; -import org.apache.fineract.portfolio.account.domain.AccountTransferStandingInstruction; -import org.apache.fineract.portfolio.account.domain.StandingInstructionRepository; -import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus; -import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlatformService; -import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService; -import org.apache.fineract.portfolio.charge.domain.Charge; -import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper; -import org.apache.fineract.portfolio.charge.domain.ChargeTimeType; -import org.apache.fineract.portfolio.client.domain.Client; -import org.apache.fineract.portfolio.client.exception.ClientNotActiveException; -import org.apache.fineract.portfolio.group.domain.Group; -import org.apache.fineract.portfolio.group.exception.GroupNotActiveException; -import org.apache.fineract.portfolio.note.domain.Note; -import org.apache.fineract.portfolio.note.domain.NoteRepository; -import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail; -import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService; -import org.apache.fineract.portfolio.savings.service.FacilityChargeAutoApplyService; -import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType; -import org.apache.fineract.portfolio.savings.SavingsApiConstants; -import org.apache.fineract.portfolio.savings.SavingsTransactionBooleanValues; -import org.apache.fineract.portfolio.savings.data.SavingsAccountChargeDataValidator; -import org.apache.fineract.portfolio.savings.data.SavingsAccountData; -import org.apache.fineract.portfolio.savings.data.SavingsAccountDataValidator; -import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionDTO; -import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData; -import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionDataValidator; -import org.apache.fineract.portfolio.savings.domain.DepositAccountOnHoldTransaction; -import org.apache.fineract.portfolio.savings.domain.DepositAccountOnHoldTransactionRepository; -import org.apache.fineract.portfolio.savings.domain.GSIMRepositoy; -import org.apache.fineract.portfolio.savings.domain.GroupSavingsIndividualMonitoring; -import org.apache.fineract.portfolio.savings.domain.SavingsAccount; -import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler; -import org.apache.fineract.portfolio.savings.domain.SavingsAccountCharge; -import org.apache.fineract.portfolio.savings.domain.SavingsAccountChargeRepositoryWrapper; -import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper; -import org.apache.fineract.portfolio.savings.domain.SavingsAccountStatusType; -import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction; -import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransactionRepository; -import org.apache.fineract.portfolio.savings.exception.PostInterestAsOnDateException; -import org.apache.fineract.portfolio.savings.exception.PostInterestAsOnDateException.PostInterestAsOnExceptionType; -import org.apache.fineract.portfolio.savings.exception.PostInterestClosingDateException; -import org.apache.fineract.portfolio.savings.exception.SavingsAccountClosingNotAllowedException; -import org.apache.fineract.portfolio.savings.exception.SavingsAccountTransactionNotFoundException; -import org.apache.fineract.portfolio.savings.exception.SavingsOfficerAssignmentException; -import org.apache.fineract.portfolio.savings.exception.SavingsOfficerUnassignmentException; -import org.apache.fineract.portfolio.savings.exception.TransactionUpdateNotAllowedException; -import org.apache.fineract.portfolio.transfer.api.TransferApiConstants; -import org.apache.fineract.useradministration.domain.AppUser; -import org.apache.fineract.useradministration.domain.AppUserRepositoryWrapper; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.CollectionUtils; - -@Slf4j -@RequiredArgsConstructor -public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements SavingsAccountWritePlatformService { - - private final PlatformSecurityContext context; - private final SavingsAccountDataValidator fromApiJsonDeserializer; - private final SavingsAccountRepositoryWrapper savingAccountRepositoryWrapper; - private final StaffRepositoryWrapper staffRepository; - private final SavingsAccountTransactionRepository savingsAccountTransactionRepository; - private final SavingsAccountAssembler savingAccountAssembler; - private final SavingsAccountTransactionDataValidator savingsAccountTransactionDataValidator; - private final SavingsAccountChargeDataValidator savingsAccountChargeDataValidator; - private final PaymentDetailWritePlatformService paymentDetailWritePlatformService; - private final JournalEntryWritePlatformService journalEntryWritePlatformService; - private final SavingsAccountDomainService savingsAccountDomainService; - private final NoteRepository noteRepository; - private final AccountTransfersReadPlatformService accountTransfersReadPlatformService; - private final AccountAssociationsReadPlatformService accountAssociationsReadPlatformService; - private final ChargeRepositoryWrapper chargeRepository; - private final SavingsAccountChargeRepositoryWrapper savingsAccountChargeRepository; - private final HolidayRepositoryWrapper holidayRepository; - private final WorkingDaysRepositoryWrapper workingDaysRepository; - private final ConfigurationDomainService configurationDomainService; - private final DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository; - private final EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService; - private final AppUserRepositoryWrapper appuserRepository; - private final StandingInstructionRepository standingInstructionRepository; - private final BusinessEventNotifierService businessEventNotifierService; - private final GSIMRepositoy gsimRepository; - private final SavingsAccountInterestPostingService savingsAccountInterestPostingService; - private final ErrorHandler errorHandler; - private final FacilityChargeAutoApplyService facilityChargeAutoApplyService; - - @Transactional - @Override - public CommandProcessingResult gsimActivate(final Long gsimId, final JsonCommand command) { - - Long parentSavingId = gsimId; - GroupSavingsIndividualMonitoring parentSavings = gsimRepository.findById(parentSavingId).orElseThrow(); - List childSavings = this.savingAccountRepositoryWrapper.findByGsimId(gsimId); - - CommandProcessingResult result = null; - int count = 0; - for (SavingsAccount account : childSavings) { - result = activate(account.getId(), command); - if (result != null) { - count++; - if (count == parentSavings.getChildAccountsCount()) { - parentSavings.setSavingsStatus(SavingsAccountStatusType.ACTIVE.getValue()); - gsimRepository.save(parentSavings); - } - } - } - return result; - } - - @Transactional - @Override - public CommandProcessingResult activate(final Long savingsId, final JsonCommand command) { - - final AppUser user = this.context.authenticatedUser(); - - this.savingsAccountTransactionDataValidator.validateActivation(command); - - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); - - // Validate minimum opening balance - BigDecimal openingBalanceForValidation = BigDecimal.ZERO; - if (command.parameterExists("openingBalance")) { - final BigDecimal ob = command.bigDecimalValueOfParameterNamed("openingBalance"); - if (ob != null) openingBalanceForValidation = ob; - } - final BigDecimal minRequiredOpening = account.getMinRequiredOpeningBalance(); - if (minRequiredOpening != null && minRequiredOpening.compareTo(BigDecimal.ZERO) > 0 - && openingBalanceForValidation.compareTo(minRequiredOpening) < 0) { - final List errors = new ArrayList<>(); - errors.add(ApiParameterError.parameterError( - "openingBalance", - "Opening balance (" + openingBalanceForValidation + ") is less than minimum required opening balance (" + minRequiredOpening + ")", - "validation.msg.savingsaccount.openingBalance.less.than.min.required", - openingBalanceForValidation, minRequiredOpening)); - throw new PlatformApiDataValidationException(errors); - } - - checkClientOrGroupActive(account); - final Set existingTransactionIds = new HashSet<>(); - final Set existingReversedTransactionIds = new HashSet<>(); - - updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); - - final Map changes = account.activate(user, command); - - entityDatatableChecksWritePlatformService.runTheCheckForProduct(savingsId, EntityTables.SAVINGS.getName(), - StatusEnum.ACTIVATE.getValue(), EntityTables.SAVINGS.getForeignKeyColumnNameOnDatatable(), account.productId()); - - if (!changes.isEmpty()) { - final Locale locale = command.extractLocale(); - final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); - processPostActiveActions(account, fmt, existingTransactionIds, existingReversedTransactionIds); - // Auto-apply facility charges based on product facility config - facilityChargeAutoApplyService.applyFacilityChargesOnActivation(account); - this.savingAccountRepositoryWrapper.saveAndFlush(account); - // Collect any newly added facility activation charges (after flush so amountOutstanding is set) - // payCharge() internally adds transactions to account.transactions via handleChargeTransactions - // Do NOT add to existingTransactionIds — postJournalEntries must include them for GL posting - facilityChargeAutoApplyService.collectActivationCharges(account); - this.savingAccountRepositoryWrapper.saveAndFlush(account); - } - - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false); - businessEventNotifierService.notifyPostBusinessEvent(new SavingsActivateBusinessEvent(account)); - - return new CommandProcessingResultBuilder() // - .withEntityId(savingsId) // - .withOfficeId(account.officeId()) // - .withClientId(account.clientId()) // - .withGroupId(account.groupId()) // - .withSavingsId(savingsId) // - .with(changes) // - .build(); - } - - @Override - public void processPostActiveActions(final SavingsAccount account, final DateTimeFormatter fmt, final Set existingTransactionIds, - final Set existingReversedTransactionIds) { - getAppUserIfPresent(); - - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - - Money amountForDeposit = account.activateWithBalance(); - boolean isRegularTransaction = false; - if (amountForDeposit.isGreaterThanZero()) { - boolean isAccountTransfer = false; - this.savingsAccountDomainService.handleDeposit(account, fmt, account.getActivationDate(), amountForDeposit.getAmount(), null, - isAccountTransfer, isRegularTransaction, false); - - updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); - } - - account.processAccountUponActivation(isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth); - List depositAccountOnHoldTransactions = null; - if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { - depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository - .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); - } - - account.validateAccountBalanceDoesNotBecomeNegative(SavingsAccountTransactionType.PAY_CHARGE.name(), - depositAccountOnHoldTransactions, false); - } - - @Transactional - @Override - public CommandProcessingResult gsimDeposit(final Long gsimId, final JsonCommand command) { - JsonArray savingsArray = command.arrayOfParameterNamed("savingsArray"); - - CommandProcessingResult result = null; - for (JsonElement element : savingsArray) { - result = deposit(element.getAsJsonObject().get("childAccountId").getAsLong(), - JsonCommand.fromExistingCommand(command, element)); - } - return result; - } - - @Transactional - @Override - public CommandProcessingResult deposit(final Long savingsId, final JsonCommand command) { - this.context.authenticatedUser(); - - this.savingsAccountTransactionDataValidator.validate(command); - boolean isGsim = false; - - final boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus(); - - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, backdatedTxnsAllowedTill); - - if (account.getGsim() != null) { - isGsim = true; - log.debug("is gsim"); - } - checkClientOrGroupActive(account); - - final Locale locale = command.extractLocale(); - final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); - - final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); - final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount"); - - this.savingsAccountTransactionDataValidator.validateTransactionWithPivotDate(transactionDate, account); - - final Map changes = new LinkedHashMap<>(); - final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); - boolean isAccountTransfer = false; - boolean isRegularTransaction = true; - final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(account, fmt, transactionDate, - transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill); - - if (isGsim && (deposit.getId() != null)) { - - log.debug("Deposit account has been created: {} ", deposit); - - GroupSavingsIndividualMonitoring gsim = gsimRepository.findById(account.getGsim().getId()).orElseThrow(); - log.debug("parent deposit : {} ", gsim.getParentDeposit()); - log.debug("child account : {} ", savingsId); - BigDecimal currentBalance = gsim.getParentDeposit(); - BigDecimal newBalance = currentBalance.add(transactionAmount); - gsim.setParentDeposit(newBalance); - gsimRepository.save(gsim); - log.debug("balance after making deposit : {} ", - gsimRepository.findById(account.getGsim().getId()).orElseThrow().getParentDeposit()); - - } - - final String noteText = command.stringValueOfParameterNamed("note"); - if (StringUtils.isNotBlank(noteText)) { - final Note note = Note.savingsTransactionNote(account, deposit, noteText); - this.noteRepository.save(note); - } - - return new CommandProcessingResultBuilder() // - .withEntityId(deposit.getId()) // - .withOfficeId(account.officeId()) // - .withClientId(account.clientId()) // - .withGroupId(account.groupId()) // - .withSavingsId(savingsId) // - .with(changes) // - .build(); - } - - private Long saveTransactionToGenerateTransactionId(final SavingsAccountTransaction transaction) { - this.savingsAccountTransactionRepository.saveAndFlush(transaction); - return transaction.getId(); - } - - @Transactional - @Override - public CommandProcessingResult withdrawal(final Long savingsId, final JsonCommand command) { - - this.savingsAccountTransactionDataValidator.validate(command); - - boolean isGsim = false; - - final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); - final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount"); - - final Locale locale = command.extractLocale(); - final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); - - final Map changes = new LinkedHashMap<>(); - final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); - - final boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus(); - - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, backdatedTxnsAllowedTill); - - if (account.getGsim() != null) { - isGsim = true; - } - checkClientOrGroupActive(account); - - this.savingsAccountTransactionDataValidator.validateTransactionWithPivotDate(transactionDate, account); - - final boolean isAccountTransfer = false; - final boolean isRegularTransaction = true; - final boolean isApplyWithdrawFee = true; - final boolean isInterestTransfer = false; - final boolean isWithdrawBalance = false; - final SavingsTransactionBooleanValues transactionBooleanValues = new SavingsTransactionBooleanValues(isAccountTransfer, - isRegularTransaction, isApplyWithdrawFee, isInterestTransfer, isWithdrawBalance); - final SavingsAccountTransaction withdrawal = this.savingsAccountDomainService.handleWithdrawal(account, fmt, transactionDate, - transactionAmount, paymentDetail, transactionBooleanValues, backdatedTxnsAllowedTill); - - if (isGsim && (withdrawal.getId() != null)) { - GroupSavingsIndividualMonitoring gsim = gsimRepository.findById(account.getGsim().getId()).orElseThrow(); - BigDecimal currentBalance = gsim.getParentDeposit().subtract(transactionAmount); - gsim.setParentDeposit(currentBalance); - gsimRepository.save(gsim); - - } - - final String noteText = command.stringValueOfParameterNamed("note"); - if (StringUtils.isNotBlank(noteText)) { - final Note note = Note.savingsTransactionNote(account, withdrawal, noteText); - this.noteRepository.save(note); - } - - return new CommandProcessingResultBuilder() // - .withEntityId(withdrawal.getId()) // - .withOfficeId(account.officeId()) // - .withClientId(account.clientId()) // - .withGroupId(account.groupId()) // - .withSavingsId(savingsId) // - .with(changes)// - .build(); - } - - @Transactional - @Override - public CommandProcessingResult applyAnnualFee(final Long savingsAccountChargeId, final Long accountId) { - getAppUserIfPresent(); - - final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository - .findOneWithNotFoundDetection(savingsAccountChargeId, accountId); - - final LocalDate currentDate = DateUtils.getBusinessLocalDate(); - final DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd MM yyyy").withZone(DateUtils.getDateTimeZoneOfTenant()); - - while (DateUtils.isBefore(savingsAccountCharge.getDueDate(), currentDate)) { - this.payCharge(savingsAccountCharge, savingsAccountCharge.getDueDate(), savingsAccountCharge.amount(), fmt, false); - } - - return new CommandProcessingResultBuilder() // - .withEntityId(savingsAccountCharge.getId()) // - .withOfficeId(savingsAccountCharge.savingsAccount().officeId()) // - .withClientId(savingsAccountCharge.savingsAccount().clientId()) // - .withGroupId(savingsAccountCharge.savingsAccount().groupId()) // - .withSavingsId(savingsAccountCharge.savingsAccount().getId()) // - .build(); - } - - @Transactional - @Override - public CommandProcessingResult calculateInterest(final Long savingsId) { - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - - final boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus(); - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, backdatedTxnsAllowedTill); - checkClientOrGroupActive(account); - - final LocalDate today = DateUtils.getBusinessLocalDate(); - final MathContext mc = new MathContext(15, MoneyHelper.getRoundingMode()); - boolean isInterestTransfer = false; - final LocalDate postInterestOnDate = null; - boolean postReversals = false; - account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals); - - if (backdatedTxnsAllowedTill) { - this.savingsAccountTransactionRepository.saveAll(account.getSavingsAccountTransactionsWithPivotConfig()); - } - - this.savingAccountRepositoryWrapper.save(account); - - return new CommandProcessingResultBuilder() // - .withEntityId(savingsId) // - .withOfficeId(account.officeId()) // - .withClientId(account.clientId()) // - .withGroupId(account.groupId()) // - .withSavingsId(savingsId) // - .build(); - } - - @Override - @Transactional - public CommandProcessingResult postInterest(final JsonCommand command) { - Long savingsId = command.getSavingsId(); - final boolean postInterestAs = command.booleanPrimitiveValueOfParameterNamed("isPostInterestAsOn"); - final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); - - final boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus(); - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, backdatedTxnsAllowedTill); - checkClientOrGroupActive(account); - - this.savingsAccountTransactionDataValidator.validateTransactionWithPivotDate(transactionDate, account); - - if (postInterestAs) { - if (transactionDate == null) { - throw new PostInterestAsOnDateException(PostInterestAsOnExceptionType.VALID_DATE); - } - if (DateUtils.isBefore(transactionDate, account.accountSubmittedOrActivationDate())) { - throw new PostInterestAsOnDateException(PostInterestAsOnExceptionType.ACTIVATION_DATE); - } - - List savingTransactions = null; - if (backdatedTxnsAllowedTill) { - savingTransactions = account.getSavingsAccountTransactionsWithPivotConfig(); - } else { - savingTransactions = account.getTransactions(); - } - - for (SavingsAccountTransaction savingTransaction : savingTransactions) { - if (DateUtils.isBefore(transactionDate, savingTransaction.getDateOf())) { - throw new PostInterestAsOnDateException(PostInterestAsOnExceptionType.LAST_TRANSACTION_DATE); - } - } - - if (DateUtils.isDateInTheFuture(transactionDate)) { - throw new PostInterestAsOnDateException(PostInterestAsOnExceptionType.FUTURE_DATE); - } - } - postInterest(account, postInterestAs, transactionDate, backdatedTxnsAllowedTill); - - businessEventNotifierService.notifyPostBusinessEvent(new SavingsPostInterestBusinessEvent(account)); - return new CommandProcessingResultBuilder() // - .withEntityId(savingsId) // - .withOfficeId(account.officeId()) // - .withClientId(account.clientId()) // - .withGroupId(account.groupId()) // - .withSavingsId(savingsId) // - .build(); - } - - @Transactional - @Override - public void postInterest(final SavingsAccount account, final boolean postInterestAs, final LocalDate transactionDate, - final boolean backdatedTxnsAllowedTill) { - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - - if (account.getNominalAnnualInterestRate().compareTo(BigDecimal.ZERO) > 0 - || (account.allowOverdraft() && account.getNominalAnnualInterestRateOverdraft().compareTo(BigDecimal.ZERO) > 0)) { - final Set existingTransactionIds = new HashSet<>(); - final Set existingReversedTransactionIds = new HashSet<>(); - - if (backdatedTxnsAllowedTill) { - updateSavingsTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); - } else { - updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); - } - - final LocalDate today = DateUtils.getBusinessLocalDate(); - final MathContext mc = new MathContext(10, MoneyHelper.getRoundingMode()); - boolean isInterestTransfer = false; - LocalDate postInterestOnDate = null; - if (postInterestAs) { - postInterestOnDate = transactionDate; - } - boolean postReversals = false; - account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, - postInterestOnDate, backdatedTxnsAllowedTill, postReversals); - - if (!backdatedTxnsAllowedTill) { - List transactions = account.getTransactions(); - for (SavingsAccountTransaction accountTransaction : transactions) { - if (accountTransaction.getId() == null) { - this.savingsAccountTransactionRepository.save(accountTransaction); - } - } - } - - if (backdatedTxnsAllowedTill) { - this.savingsAccountTransactionRepository.saveAll(account.getSavingsAccountTransactionsWithPivotConfig()); - } - - this.savingAccountRepositoryWrapper.saveAndFlush(account); - - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, backdatedTxnsAllowedTill); - } - } - - @Transactional - @Override - @Retry(name = "postInterest", fallbackMethod = "fallbackPostInterest") - public SavingsAccountData postInterest(SavingsAccountData savingsAccountData, final boolean postInterestAs, - final LocalDate transactionDate, final boolean backdatedTxnsAllowedTill) { - - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - - if (MathUtil.isGreaterThanZero(savingsAccountData.getNominalAnnualInterestRate()) || (savingsAccountData.isAllowOverdraft() - && MathUtil.isGreaterThanZero(savingsAccountData.getNominalAnnualInterestRateOverdraft()))) { - final Set existingTransactionIds = new HashSet<>(); - final Set existingReversedTransactionIds = new HashSet<>(); - updateExistingTransactionsDetails(savingsAccountData, existingTransactionIds, existingReversedTransactionIds); - - final LocalDate today = DateUtils.getBusinessLocalDate(); - final MathContext mc = new MathContext(10, MoneyHelper.getRoundingMode()); - boolean isInterestTransfer = false; - LocalDate postInterestOnDate = null; - if (postInterestAs) { - postInterestOnDate = transactionDate; - } - - savingsAccountData = this.savingsAccountInterestPostingService.postInterest(mc, today, isInterestTransfer, - isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, - savingsAccountData); - - if (!backdatedTxnsAllowedTill) { - List transactions = savingsAccountData.getSavingsAccountTransactionData(); - for (SavingsAccountTransactionData accountTransaction : transactions) { - if (accountTransaction.getId() == null) { - savingsAccountData.setNewSavingsAccountTransactionData(accountTransaction); - selectAccountId(accountTransaction, savingsAccountData); - } - } - } - savingsAccountData.setExistingTransactionIds(existingTransactionIds); - savingsAccountData.setExistingReversedTransactionIds(existingReversedTransactionIds); - } - return savingsAccountData; - } - - public void selectAccountId(SavingsAccountTransactionData accountTransaction, SavingsAccountData savingsAccountData) { - SavingsAccountTransactionType transactionType = SavingsAccountTransactionType - .fromInt(accountTransaction.getTransactionType().getId().intValue()); - if (transactionType.isOverDraftInterestPosting()) { - if (MathUtil.isGreaterThanZero(accountTransaction.getRunningBalance())) { - accountTransaction.setAccountDebit(savingsAccountData.getGlAccountIdForSavingsControl()); - accountTransaction.setAccountCredit(savingsAccountData.getGlAccountIdForInterestReceivable()); - } else { - accountTransaction.setAccountDebit(savingsAccountData.getGlAccountIdForOverdraftPorfolio()); - accountTransaction.setAccountCredit(savingsAccountData.getGlAccountIdForInterestReceivable()); - } - } else { - accountTransaction.setAccountDebit(savingsAccountData.getGlAccountIdForInterestPayable()); - accountTransaction.setAccountCredit(savingsAccountData.getGlAccountIdForSavingsControl()); - } - } - - @Override - public CommandProcessingResult reverseTransaction(final Long savingsId, final Long transactionId, - final boolean allowAccountTransferModification, final JsonCommand command) { - - final boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus(); - final boolean isBulk = command.booleanPrimitiveValueOfParameterNamed("isBulk"); - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, backdatedTxnsAllowedTill); - - final SavingsAccountTransaction savingsAccountTransaction = this.savingsAccountTransactionRepository - .findOneByIdAndSavingsAccountId(transactionId, savingsId); - if (savingsAccountTransaction == null) { - throw new SavingsAccountTransactionNotFoundException(savingsId, transactionId); - } - - if (!allowAccountTransferModification - && this.accountTransfersReadPlatformService.isAccountTransfer(transactionId, PortfolioAccountType.SAVINGS)) { - throw new PlatformServiceUnavailableException("error.msg.saving.account.transfer.transaction.update.not.allowed", - "Savings account transaction:" + transactionId + " update not allowed as it involves in account transfer", - transactionId); - } - - if (!account.allowModify()) { - throw new PlatformServiceUnavailableException("error.msg.saving.account.transaction.update.not.allowed", - "Savings account transaction:" + transactionId + " update not allowed for this savings type", transactionId); - } - if (account.isNotActive()) { - throwValidationForActiveStatus(SavingsApiConstants.undoTransactionAction); - } - SavingsAccountTransaction reversal = null; - Long reversalId = null; - checkClientOrGroupActive(account); - List savingsAccountTransactions = null; - if (isBulk) { - String transactionRefNo = savingsAccountTransaction.getRefNo(); - savingsAccountTransactions = this.savingsAccountTransactionRepository.findByRefNo(transactionRefNo); - reversal = this.savingsAccountDomainService.handleReversal(account, savingsAccountTransactions, backdatedTxnsAllowedTill); - } else { - reversal = this.savingsAccountDomainService.handleReversal(account, Collections.singletonList(savingsAccountTransaction), - backdatedTxnsAllowedTill); - } - reversalId = reversal.getId(); - return new CommandProcessingResultBuilder() // - .withEntityId(reversalId) // - .withOfficeId(account.officeId()) // - .withClientId(account.clientId()) // - .withGroupId(account.groupId()) // - .withSavingsId(savingsId) // - .build(); - } - - @Override - public CommandProcessingResult undoTransaction(final Long savingsId, final Long transactionId, - final boolean allowAccountTransferModification) { - - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); - final Set existingTransactionIds = new HashSet<>(); - final Set existingReversedTransactionIds = new HashSet<>(); - updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); - - final SavingsAccountTransaction savingsAccountTransaction = this.savingsAccountTransactionRepository - .findOneByIdAndSavingsAccountId(transactionId, savingsId); - if (savingsAccountTransaction == null) { - throw new SavingsAccountTransactionNotFoundException(savingsId, transactionId); - } - - this.savingsAccountTransactionDataValidator.validateTransactionWithPivotDate(savingsAccountTransaction.getTransactionDate(), - account); - - if (!allowAccountTransferModification - && this.accountTransfersReadPlatformService.isAccountTransfer(transactionId, PortfolioAccountType.SAVINGS)) { - throw new PlatformServiceUnavailableException("error.msg.saving.account.transfer.transaction.update.not.allowed", - "Savings account transaction:" + transactionId + " update not allowed as it involves in account transfer", - transactionId); - } - - if (!account.allowModify()) { - throw new PlatformServiceUnavailableException("error.msg.saving.account.transaction.update.not.allowed", - "Savings account transaction:" + transactionId + " update not allowed for this savings type", transactionId); - } - - final LocalDate today = DateUtils.getBusinessLocalDate(); - final MathContext mc = new MathContext(15, MoneyHelper.getRoundingMode()); - - if (account.isNotActive()) { - throwValidationForActiveStatus(SavingsApiConstants.undoTransactionAction); - } - account.undoTransaction(transactionId); - - // undoing transaction is withdrawal then undo withdrawal fee - // transaction if any - if (savingsAccountTransaction.isWithdrawal()) { - final SavingsAccountTransaction nextSavingsAccountTransaction = this.savingsAccountTransactionRepository - .findOneByIdAndSavingsAccountId(transactionId + 1, savingsId); - if (nextSavingsAccountTransaction != null && nextSavingsAccountTransaction.isWithdrawalFeeAndNotReversed()) { - account.undoTransaction(transactionId + 1); - } - } - boolean isInterestTransfer = false; - LocalDate postInterestOnDate = null; - boolean postReversals = false; - checkClientOrGroupActive(account); - if (savingsAccountTransaction.isPostInterestCalculationRequired() - && account.isBeforeLastPostingPeriod(savingsAccountTransaction.getTransactionDate(), false)) { - account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, - postInterestOnDate, false, postReversals); - } else { - account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate, false, postReversals); - } - List depositAccountOnHoldTransactions = null; - if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { - depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository - .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); - } - account.validateAccountBalanceDoesNotBecomeNegative(SavingsApiConstants.undoTransactionAction, depositAccountOnHoldTransactions, - false); - account.activateAccountBasedOnBalance(); - this.savingAccountRepositoryWrapper.saveAndFlush(account); - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false); - return new CommandProcessingResultBuilder() // - .withEntityId(savingsId) // - .withOfficeId(account.officeId()) // - .withClientId(account.clientId()) // - .withGroupId(account.groupId()) // - .withSavingsId(savingsId) // - .build(); - } - - @Override - public CommandProcessingResult adjustSavingsTransaction(final Long savingsId, final Long transactionId, final JsonCommand command) { - context.authenticatedUser(); - - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - final Long relaxingDaysConfigForPivotDate = this.configurationDomainService.retrieveRelaxingDaysConfigForPivotDate(); - final SavingsAccountTransaction savingsAccountTransaction = this.savingsAccountTransactionRepository - .findOneByIdAndSavingsAccountId(transactionId, savingsId); - if (savingsAccountTransaction == null) { - throw new SavingsAccountTransactionNotFoundException(savingsId, transactionId); - } - - if (!(savingsAccountTransaction.isDeposit() || savingsAccountTransaction.isWithdrawal()) - || savingsAccountTransaction.isReversed()) { - throw new TransactionUpdateNotAllowedException(savingsId, transactionId); - } - - if (this.accountTransfersReadPlatformService.isAccountTransfer(transactionId, PortfolioAccountType.SAVINGS)) { - throw new PlatformServiceUnavailableException("error.msg.saving.account.transfer.transaction.update.not.allowed", - "Savings account transaction:" + transactionId + " update not allowed as it involves in account transfer", - transactionId); - } - - this.savingsAccountTransactionDataValidator.validate(command); - - final LocalDate today = DateUtils.getBusinessLocalDate(); - - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); - - if (account.isNotActive()) { - throwValidationForActiveStatus(SavingsApiConstants.adjustTransactionAction); - } - if (!account.allowModify()) { - throw new PlatformServiceUnavailableException("error.msg.saving.account.transaction.update.not.allowed", - "Savings account transaction:" + transactionId + " update not allowed for this savings type", transactionId); - } - final Set existingTransactionIds = new HashSet<>(); - final Set existingReversedTransactionIds = new HashSet<>(); - - updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); - - final Locale locale = command.extractLocale(); - final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); - final LocalDate transactionDate = command.localDateValueOfParameterNamed(SavingsApiConstants.transactionDateParamName); - final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed(SavingsApiConstants.transactionAmountParamName); - final Map changes = new LinkedHashMap<>(); - final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); - - final MathContext mc = new MathContext(10, MoneyHelper.getRoundingMode()); - account.undoTransaction(transactionId); - - // for undo withdrawal fee - final SavingsAccountTransaction nextSavingsAccountTransaction = this.savingsAccountTransactionRepository - .findOneByIdAndSavingsAccountId(transactionId + 1, savingsId); - if (nextSavingsAccountTransaction != null && nextSavingsAccountTransaction.isWithdrawalFeeAndNotReversed()) { - account.undoTransaction(transactionId + 1); - } - - SavingsAccountTransaction transaction = null; - boolean isInterestTransfer = false; - Integer accountType = null; - final SavingsAccountTransactionDTO transactionDTO = new SavingsAccountTransactionDTO(fmt, transactionDate, transactionAmount, - paymentDetail, null, accountType); - UUID refNo = UUID.randomUUID(); - if (savingsAccountTransaction.isDeposit()) { - transaction = account.deposit(transactionDTO, false, relaxingDaysConfigForPivotDate, refNo.toString()); - } else { - transaction = account.withdraw(transactionDTO, true, false, relaxingDaysConfigForPivotDate, refNo.toString()); - } - final Long newtransactionId = saveTransactionToGenerateTransactionId(transaction); - final LocalDate postInterestOnDate = null; - boolean postReversals = false; - if (account.isBeforeLastPostingPeriod(transactionDate, false) - || account.isBeforeLastPostingPeriod(savingsAccountTransaction.getTransactionDate(), false)) { - account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, - postInterestOnDate, false, postReversals); - } else { - account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate, false, postReversals); - } - List depositAccountOnHoldTransactions = null; - if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { - depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository - .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); - } - - account.validateAccountBalanceDoesNotBecomeNegative(SavingsApiConstants.adjustTransactionAction, depositAccountOnHoldTransactions, - false); - account.activateAccountBasedOnBalance(); - - this.savingAccountRepositoryWrapper.saveAndFlush(account); - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false); - return new CommandProcessingResultBuilder() // - .withEntityId(newtransactionId) // - .withOfficeId(account.officeId()) // - .withClientId(account.clientId()) // - .withGroupId(account.groupId()) // - .withSavingsId(savingsId) // - .with(changes)// - .build(); - } - - /** - * - */ - private void throwValidationForActiveStatus(final String actionName) { - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(SAVINGS_ACCOUNT_RESOURCE_NAME + actionName); - baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("account.is.not.active"); - throw new PlatformApiDataValidationException(dataValidationErrors); - } - - private void checkClientOrGroupActive(final SavingsAccount account) { - final Client client = account.getClient(); - if (client != null) { - if (client.isNotActive()) { - throw new ClientNotActiveException(client.getId()); - } - } - final Group group = account.group(); - if (group != null) { - if (group.isNotActive()) { - throw new GroupNotActiveException(group.getId()); - } - } - } - - @Override - public CommandProcessingResult bulkGSIMClose(final Long gsimId, final JsonCommand command) { - - final Long parentSavingId = gsimId; - GroupSavingsIndividualMonitoring parentSavings = gsimRepository.findById(parentSavingId).orElseThrow(); - List childSavings = this.savingAccountRepositoryWrapper.findByGsimId(gsimId); - - CommandProcessingResult result = null; - int count = 0; - for (SavingsAccount account : childSavings) { - result = close(account.getId(), command); - - if (result != null) { - count++; - if (count == parentSavings.getChildAccountsCount()) { - parentSavings.setSavingsStatus(SavingsAccountStatusType.CLOSED.getValue()); - gsimRepository.save(parentSavings); - } - } - } - return result; - } - - @Override - public CommandProcessingResult close(final Long savingsId, final JsonCommand command) { - final AppUser user = this.context.authenticatedUser(); - - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); - this.savingsAccountTransactionDataValidator.validateClosing(command, account); - - final boolean isLinkedWithAnyActiveLoan = this.accountAssociationsReadPlatformService.isLinkedWithAnyActiveAccount(savingsId); - - if (isLinkedWithAnyActiveLoan) { - final String defaultUserMessage = "Closing savings account with id:" + savingsId - + " is not allowed, since it is linked with one of the active accounts"; - throw new SavingsAccountClosingNotAllowedException("linked", defaultUserMessage, savingsId); - } - - entityDatatableChecksWritePlatformService.runTheCheckForProduct(savingsId, EntityTables.SAVINGS.getName(), - StatusEnum.CLOSE.getValue(), EntityTables.SAVINGS.getForeignKeyColumnNameOnDatatable(), account.productId()); - - final boolean isWithdrawBalance = command.booleanPrimitiveValueOfParameterNamed(withdrawBalanceParamName); - - final Locale locale = command.extractLocale(); - final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); - final LocalDate closedDate = command.localDateValueOfParameterNamed(SavingsApiConstants.closedOnDateParamName); - final boolean isPostInterest = command.booleanPrimitiveValueOfParameterNamed(SavingsApiConstants.postInterestValidationOnClosure); - // postInterest(account,closedDate,flag); - if (isPostInterest) { - boolean postInterestOnClosingDate = false; - List savingTransactions = account.getTransactions(); - for (SavingsAccountTransaction savingTransaction : savingTransactions) { - if (savingTransaction.isInterestPosting() && savingTransaction.isNotReversed() - && DateUtils.isEqual(closedDate, savingTransaction.getTransactionDate())) { - postInterestOnClosingDate = true; - break; - } - } - if (!postInterestOnClosingDate) { - throw new PostInterestClosingDateException(); - } - } - - final Map changes = new LinkedHashMap<>(); - - if (isWithdrawBalance && account.getSummary().getAccountBalance(account.getCurrency()).isGreaterThanZero()) { - - final BigDecimal transactionAmount = account.getSummary().getAccountBalance(); - - final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); - - final boolean isAccountTransfer = false; - final boolean isRegularTransaction = true; - final boolean isApplyWithdrawFee = false; - final boolean isInterestTransfer = false; - final SavingsTransactionBooleanValues transactionBooleanValues = new SavingsTransactionBooleanValues(isAccountTransfer, - isRegularTransaction, isApplyWithdrawFee, isInterestTransfer, isWithdrawBalance); - - this.savingsAccountDomainService.handleWithdrawal(account, fmt, closedDate, transactionAmount, paymentDetail, - transactionBooleanValues, false); - - } - - final Map accountChanges = account.close(user, command); - changes.putAll(accountChanges); - if (!changes.isEmpty()) { - this.savingAccountRepositoryWrapper.save(account); - final String noteText = command.stringValueOfParameterNamed("note"); - if (StringUtils.isNotBlank(noteText)) { - final Note note = Note.savingNote(account, noteText); - changes.put("note", noteText); - this.noteRepository.save(note); - } - - } - - businessEventNotifierService.notifyPostBusinessEvent(new SavingsCloseBusinessEvent(account)); - // disable all standing orders linked to the savings account - disableStandingInstructionsLinkedToClosedSavings(account); - return new CommandProcessingResultBuilder() // - .withEntityId(savingsId) // - .withOfficeId(account.officeId()) // - .withClientId(account.clientId()) // - .withGroupId(account.groupId()) // - .withSavingsId(savingsId) // - .with(changes) // - .build(); - } - - @Override - public SavingsAccountTransaction initiateSavingsTransfer(final SavingsAccount savingsAccount, final LocalDate transferDate) { - getAppUserIfPresent(); - - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - - validateTransactionsForTransfer(savingsAccount, transferDate); - - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - this.savingAccountAssembler.setHelpers(savingsAccount); - final Set existingTransactionIds = new HashSet<>(); - final Set existingReversedTransactionIds = new HashSet<>(); - updateExistingTransactionsDetails(savingsAccount, existingTransactionIds, existingReversedTransactionIds); - - final SavingsAccountTransaction newTransferTransaction = SavingsAccountTransaction.initiateTransfer(savingsAccount, - savingsAccount.office(), transferDate); - savingsAccount.addTransaction(newTransferTransaction); - savingsAccount.setStatus(SavingsAccountStatusType.TRANSFER_IN_PROGRESS.getValue()); - final MathContext mc = MathContext.DECIMAL64; - boolean isInterestTransfer = false; - LocalDate postInterestOnDate = null; - boolean postReversals = false; - savingsAccount.calculateInterestUsing(mc, transferDate, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate, false, postReversals); - - this.savingsAccountTransactionRepository.save(newTransferTransaction); - this.savingAccountRepositoryWrapper.saveAndFlush(savingsAccount); - - postJournalEntries(savingsAccount, existingTransactionIds, existingReversedTransactionIds, false); - - return newTransferTransaction; - } - - @Override - public SavingsAccountTransaction withdrawSavingsTransfer(final SavingsAccount savingsAccount, final LocalDate transferDate) { - getAppUserIfPresent(); - - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - this.savingAccountAssembler.setHelpers(savingsAccount); - final Set existingTransactionIds = new HashSet<>(); - final Set existingReversedTransactionIds = new HashSet<>(); - updateExistingTransactionsDetails(savingsAccount, existingTransactionIds, existingReversedTransactionIds); - - final SavingsAccountTransaction withdrawtransferTransaction = SavingsAccountTransaction.withdrawTransfer(savingsAccount, - savingsAccount.office(), transferDate); - savingsAccount.addTransaction(withdrawtransferTransaction); - savingsAccount.setStatus(SavingsAccountStatusType.ACTIVE.getValue()); - final MathContext mc = MathContext.DECIMAL64; - boolean isInterestTransfer = false; - LocalDate postInterestOnDate = null; - boolean postReversals = false; - savingsAccount.calculateInterestUsing(mc, transferDate, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate, false, postReversals); - - this.savingsAccountTransactionRepository.save(withdrawtransferTransaction); - this.savingAccountRepositoryWrapper.saveAndFlush(savingsAccount); - - postJournalEntries(savingsAccount, existingTransactionIds, existingReversedTransactionIds, false); - - return withdrawtransferTransaction; - } - - @Override - public void rejectSavingsTransfer(final SavingsAccount savingsAccount) { - this.savingAccountAssembler.setHelpers(savingsAccount); - savingsAccount.setStatus(SavingsAccountStatusType.TRANSFER_ON_HOLD.getValue()); - this.savingAccountRepositoryWrapper.save(savingsAccount); - } - - @Override - public SavingsAccountTransaction acceptSavingsTransfer(final SavingsAccount savingsAccount, final LocalDate transferDate, - final Office acceptedInOffice, final Staff fieldOfficer) { - getAppUserIfPresent(); - - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - this.savingAccountAssembler.setHelpers(savingsAccount); - final Set existingTransactionIds = new HashSet<>(); - final Set existingReversedTransactionIds = new HashSet<>(); - updateExistingTransactionsDetails(savingsAccount, existingTransactionIds, existingReversedTransactionIds); - - final SavingsAccountTransaction acceptTransferTransaction = SavingsAccountTransaction.approveTransfer(savingsAccount, - acceptedInOffice, transferDate); - savingsAccount.addTransaction(acceptTransferTransaction); - savingsAccount.setStatus(SavingsAccountStatusType.ACTIVE.getValue()); - if (fieldOfficer != null) { - savingsAccount.reassignSavingsOfficer(fieldOfficer, transferDate); - } - boolean isInterestTransfer = false; - final MathContext mc = MathContext.DECIMAL64; - LocalDate postInterestOnDate = null; - boolean postReversals = false; - savingsAccount.calculateInterestUsing(mc, transferDate, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate, false, postReversals); - - this.savingsAccountTransactionRepository.save(acceptTransferTransaction); - this.savingAccountRepositoryWrapper.saveAndFlush(savingsAccount); - - postJournalEntries(savingsAccount, existingTransactionIds, existingReversedTransactionIds, false); - - return acceptTransferTransaction; - } - - @Transactional - @Override - public CommandProcessingResult addSavingsAccountCharge(final JsonCommand command) { - this.context.authenticatedUser(); - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(SAVINGS_ACCOUNT_RESOURCE_NAME); - - final Long savingsAccountId = command.getSavingsId(); - this.savingsAccountChargeDataValidator.validateAdd(command.json()); - - final SavingsAccount savingsAccount = this.savingAccountAssembler.assembleFrom(savingsAccountId, false); - checkClientOrGroupActive(savingsAccount); - - final Locale locale = command.extractLocale(); - final String format = command.dateFormat(); - final DateTimeFormatter fmt = StringUtils.isNotBlank(format) ? DateTimeFormatter.ofPattern(format).withLocale(locale) - : DateTimeFormatter.ofPattern("dd MM yyyy"); - - final Long chargeDefinitionId = command.longValueOfParameterNamed(chargeIdParamName); - final Charge chargeDefinition = this.chargeRepository.findOneWithNotFoundDetection(chargeDefinitionId); - - Integer chargeTimeType = chargeDefinition.getChargeTimeType(); - LocalDate dueAsOfDateParam = command.localDateValueOfParameterNamed(dueAsOfDateParamName); - if ((chargeTimeType.equals(ChargeTimeType.WITHDRAWAL_FEE.getValue()) - || chargeTimeType.equals(ChargeTimeType.SAVINGS_NOACTIVITY_FEE.getValue())) && dueAsOfDateParam != null) { - baseDataValidator.reset().parameter(dueAsOfDateParamName).value(dueAsOfDateParam.format(fmt)) - .failWithCodeNoParameterAddedToErrorCode( - "charge.due.date.is.invalid.for." + ChargeTimeType.fromInt(chargeTimeType).getCode()); - } - final SavingsAccountCharge savingsAccountCharge = SavingsAccountCharge.createNewFromJson(savingsAccount, chargeDefinition, command); - - if (chargeDefinition.isEnableFreeWithdrawal()) { - savingsAccountCharge.setFreeWithdrawalCount(0); - } - - if (savingsAccountCharge.getDueDate() != null) { - // transaction date should not be on a holiday or non working day - if (!this.configurationDomainService.allowTransactionsOnHolidayEnabled() - && this.holidayRepository.isHoliday(savingsAccount.officeId(), savingsAccountCharge.getDueDate())) { - baseDataValidator.reset().parameter(dueAsOfDateParamName).value(savingsAccountCharge.getDueDate().format(fmt)) - .failWithCodeNoParameterAddedToErrorCode("charge.due.date.is.on.holiday"); - } - - if (!this.configurationDomainService.allowTransactionsOnNonWorkingDayEnabled() - && !this.workingDaysRepository.isWorkingDay(savingsAccountCharge.getDueDate())) { - baseDataValidator.reset().parameter(dueAsOfDateParamName).value(savingsAccountCharge.getDueDate().format(fmt)) - .failWithCodeNoParameterAddedToErrorCode("charge.due.date.is.a.nonworking.day"); - } - } - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - - savingsAccount.addCharge(fmt, savingsAccountCharge, chargeDefinition); - this.savingsAccountChargeRepository.save(savingsAccountCharge); - this.savingAccountRepositoryWrapper.saveAndFlush(savingsAccount); - return new CommandProcessingResultBuilder() // - .withEntityId(savingsAccountCharge.getId()) // - .withOfficeId(savingsAccount.officeId()) // - .withClientId(savingsAccount.clientId()) // - .withGroupId(savingsAccount.groupId()) // - .withSavingsId(savingsAccountId) // - .build(); - } - - @Transactional - @Override - public CommandProcessingResult updateSavingsAccountCharge(final JsonCommand command) { - - this.context.authenticatedUser(); - this.savingsAccountChargeDataValidator.validateUpdate(command.json()); - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(SAVINGS_ACCOUNT_RESOURCE_NAME); - - final Long savingsAccountId = command.getSavingsId(); - // SavingsAccount Charge entity - final Long savingsChargeId = command.entityId(); - - final SavingsAccount savingsAccount = this.savingAccountAssembler.assembleFrom(savingsAccountId, false); - checkClientOrGroupActive(savingsAccount); - - final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository.findOneWithNotFoundDetection(savingsChargeId, - savingsAccountId); - - final Map changes = savingsAccountCharge.update(command); - - if (savingsAccountCharge.getDueDate() != null) { - final Locale locale = command.extractLocale(); - final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); - - // transaction date should not be on a holiday or non working day - if (!this.configurationDomainService.allowTransactionsOnHolidayEnabled() - && this.holidayRepository.isHoliday(savingsAccount.officeId(), savingsAccountCharge.getDueDate())) { - baseDataValidator.reset().parameter(dueAsOfDateParamName).value(savingsAccountCharge.getDueDate().format(fmt)) - .failWithCodeNoParameterAddedToErrorCode("charge.due.date.is.on.holiday"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - if (!this.configurationDomainService.allowTransactionsOnNonWorkingDayEnabled() - && !this.workingDaysRepository.isWorkingDay(savingsAccountCharge.getDueDate())) { - baseDataValidator.reset().parameter(dueAsOfDateParamName).value(savingsAccountCharge.getDueDate().format(fmt)) - .failWithCodeNoParameterAddedToErrorCode("charge.due.date.is.a.nonworking.day"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - } - - this.savingsAccountChargeRepository.saveAndFlush(savingsAccountCharge); - - return new CommandProcessingResultBuilder() // - .withEntityId(savingsAccountCharge.getId()) // - .withOfficeId(savingsAccountCharge.savingsAccount().officeId()) // - .withClientId(savingsAccountCharge.savingsAccount().clientId()) // - .withGroupId(savingsAccountCharge.savingsAccount().groupId()) // - .withSavingsId(savingsAccountCharge.savingsAccount().getId()) // - .with(changes) // - .build(); - } - - @Transactional - @Override - public CommandProcessingResult waiveCharge(final Long savingsAccountId, final Long savingsAccountChargeId) { - context.authenticatedUser(); - - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - - final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository - .findOneWithNotFoundDetection(savingsAccountChargeId, savingsAccountId); - - // Get Savings account from savings charge - final SavingsAccount account = savingsAccountCharge.savingsAccount(); - - final boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus(); - - this.savingAccountAssembler.loadTransactionsToSavingsAccount(account, backdatedTxnsAllowedTill); - - final Set existingTransactionIds = new HashSet<>(); - final Set existingReversedTransactionIds = new HashSet<>(); - - if (backdatedTxnsAllowedTill) { - updateSavingsTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); - } else { - updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); - } - - account.waiveCharge(savingsAccountChargeId, backdatedTxnsAllowedTill); - - boolean isInterestTransfer = false; - LocalDate postInterestOnDate = null; - final MathContext mc = MathContext.DECIMAL64; - boolean postReversals = false; - if (account.isBeforeLastPostingPeriod(savingsAccountCharge.getDueDate(), backdatedTxnsAllowedTill)) { - final LocalDate today = DateUtils.getBusinessLocalDate(); - account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, - postInterestOnDate, backdatedTxnsAllowedTill, postReversals); - } else { - final LocalDate today = DateUtils.getBusinessLocalDate(); - account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals); - } - - List depositAccountOnHoldTransactions = null; - if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { - depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository - .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); - } - - account.validateAccountBalanceDoesNotBecomeNegative(SavingsApiConstants.waiveChargeTransactionAction, - depositAccountOnHoldTransactions, backdatedTxnsAllowedTill); - - if (backdatedTxnsAllowedTill) { - this.savingsAccountTransactionRepository.saveAll(account.getSavingsAccountTransactionsWithPivotConfig()); - } - - this.savingAccountRepositoryWrapper.saveAndFlush(account); - - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, backdatedTxnsAllowedTill); - return new CommandProcessingResultBuilder() // - .withEntityId(savingsAccountChargeId) // - .withOfficeId(account.officeId()) // - .withClientId(account.clientId()) // - .withGroupId(account.groupId()) // - .withSavingsId(savingsAccountId) // - .build(); - } - - @Transactional - @Override - public CommandProcessingResult deleteSavingsAccountCharge(final Long savingsAccountId, final Long savingsAccountChargeId, - @SuppressWarnings("unused") final JsonCommand command) { - this.context.authenticatedUser(); - - final SavingsAccount savingsAccount = this.savingAccountAssembler.assembleFrom(savingsAccountId, false); - checkClientOrGroupActive(savingsAccount); - final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository - .findOneWithNotFoundDetection(savingsAccountChargeId, savingsAccountId); - - savingsAccount.removeCharge(savingsAccountCharge); - this.savingAccountRepositoryWrapper.saveAndFlush(savingsAccount); - - return new CommandProcessingResultBuilder() // - .withEntityId(savingsAccountChargeId) // - .withOfficeId(savingsAccount.officeId()) // - .withClientId(savingsAccount.clientId()) // - .withGroupId(savingsAccount.groupId()) // - .withSavingsId(savingsAccountId) // - .build(); - } - - @Override - public CommandProcessingResult payCharge(final Long savingsAccountId, final Long savingsAccountChargeId, final JsonCommand command) { - context.authenticatedUser(); - - this.savingsAccountChargeDataValidator.validatePayCharge(command.json()); - final Locale locale = command.extractLocale(); - final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); - final BigDecimal amountPaid = command.bigDecimalValueOfParameterNamed(amountParamName); - final LocalDate transactionDate = command.localDateValueOfParameterNamed(dueAsOfDateParamName); - - final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository - .findOneWithNotFoundDetection(savingsAccountChargeId, savingsAccountId); - - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(SAVINGS_ACCOUNT_RESOURCE_NAME); - - // transaction date should not be on a holiday or non working day - if (!this.configurationDomainService.allowTransactionsOnHolidayEnabled() - && this.holidayRepository.isHoliday(savingsAccountCharge.savingsAccount().officeId(), transactionDate)) { - baseDataValidator.reset().parameter(dueAsOfDateParamName).value(transactionDate.format(fmt)) - .failWithCodeNoParameterAddedToErrorCode("transaction.not.allowed.transaction.date.is.on.holiday"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - if (!this.configurationDomainService.allowTransactionsOnNonWorkingDayEnabled() - && !this.workingDaysRepository.isWorkingDay(transactionDate)) { - baseDataValidator.reset().parameter(dueAsOfDateParamName).value(transactionDate.format(fmt)) - .failWithCodeNoParameterAddedToErrorCode("transaction.not.allowed.transaction.date.is.a.nonworking.day"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - final boolean backdatedTxnsAllowedTill = false; - - SavingsAccountTransaction chargeTransaction = this.payCharge(savingsAccountCharge, transactionDate, amountPaid, fmt, - backdatedTxnsAllowedTill); - - final String noteText = command.stringValueOfParameterNamed("note"); - if (StringUtils.isNotBlank(noteText)) { - final Note note = Note.savingsTransactionNote(savingsAccountCharge.savingsAccount(), chargeTransaction, noteText); - this.noteRepository.save(note); - } - - return new CommandProcessingResultBuilder() // - .withEntityId(savingsAccountCharge.getId()) // - .withOfficeId(savingsAccountCharge.savingsAccount().officeId()) // - .withClientId(savingsAccountCharge.savingsAccount().clientId()) // - .withGroupId(savingsAccountCharge.savingsAccount().groupId()) // - .withSavingsId(savingsAccountCharge.savingsAccount().getId()) // - .build(); - - } - - @Transactional - @Override - public void applyChargeDue(final Long savingsAccountChargeId, final Long accountId) { - // always use current date as transaction date for batch job - final LocalDate transactionDate = DateUtils.getBusinessLocalDate(); - final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository - .findOneWithNotFoundDetection(savingsAccountChargeId, accountId); - - final DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd MM yyyy").withZone(DateUtils.getDateTimeZoneOfTenant()); - - while (savingsAccountCharge.isNotFullyPaid() && DateUtils.isBefore(savingsAccountCharge.getDueDate(), transactionDate)) { - payCharge(savingsAccountCharge, transactionDate, savingsAccountCharge.amoutOutstanding(), fmt, false); - } - } - - @SuppressWarnings("unused") - public SavingsAccountData fallbackPostInterest(SavingsAccountData savingsAccountData, boolean postInterestAs, LocalDate transactionDate, - boolean backdatedTxnsAllowedTill, Throwable t) { - // NOTE: allow caller to catch the exceptions - throw org.apache.fineract.infrastructure.core.exception.ErrorHandler.getMappable(t, null, null, "savings.postinterest"); - } - - @Transactional - private SavingsAccountTransaction payCharge(final SavingsAccountCharge savingsAccountCharge, final LocalDate transactionDate, - final BigDecimal amountPaid, final DateTimeFormatter formatter, final boolean backdatedTxnsAllowedTill) { - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - - // Get Savings account from savings charge - final SavingsAccount account = savingsAccountCharge.savingsAccount(); - this.savingAccountAssembler.assignSavingAccountHelpers(account); - final Set existingTransactionIds = new HashSet<>(); - final Set existingReversedTransactionIds = new HashSet<>(); - Pageable sortedByDateAndIdDesc = PageRequest.of(0, 1, Sort.by("dateOf", "id").descending()); - - List savingsAccountTransaction = this.savingsAccountTransactionRepository - .findBySavingsAccountIdAndLessThanDateOfAndReversedIsFalse(account.getId(), transactionDate, sortedByDateAndIdDesc); - - account.validateAccountBalanceDoesNotViolateOverdraft(savingsAccountTransaction, amountPaid); - - updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); - SavingsAccountTransaction chargeTransaction = account.payCharge(savingsAccountCharge, amountPaid, transactionDate, formatter, - backdatedTxnsAllowedTill, null); - boolean isInterestTransfer = false; - LocalDate postInterestOnDate = null; - final MathContext mc = MathContext.DECIMAL64; - boolean postReversals = false; - if (account.isBeforeLastPostingPeriod(transactionDate, backdatedTxnsAllowedTill)) { - final LocalDate today = DateUtils.getBusinessLocalDate(); - account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, - postInterestOnDate, isInterestTransfer, postReversals); - } else { - final LocalDate today = DateUtils.getBusinessLocalDate(); - account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals); - } - List depositAccountOnHoldTransactions = null; - - if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { - depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository - .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); - } - - account.validateAccountBalanceDoesNotBecomeNegative("." + SavingsAccountTransactionType.PAY_CHARGE.getCode(), - depositAccountOnHoldTransactions, backdatedTxnsAllowedTill); - - saveTransactionToGenerateTransactionId(chargeTransaction); - - this.savingAccountRepositoryWrapper.saveAndFlush(account); - - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, backdatedTxnsAllowedTill); - - return chargeTransaction; - } - - private void updateExistingTransactionsDetails(SavingsAccount account, Set existingTransactionIds, - Set existingReversedTransactionIds) { - existingTransactionIds.addAll(account.findExistingTransactionIds()); - existingReversedTransactionIds.addAll(account.findExistingReversedTransactionIds()); - } - - private void updateExistingTransactionsDetails(SavingsAccountData account, Set existingTransactionIds, - Set existingReversedTransactionIds) { - existingTransactionIds.addAll(account.findCurrentTransactionIdsWithPivotDateConfig()); - existingReversedTransactionIds.addAll(account.findCurrentReversedTransactionIdsWithPivotDateConfig()); - } - - private void updateSavingsTransactionsDetails(SavingsAccount account, Set existingTransactionIds, - Set existingReversedTransactionIds) { - existingTransactionIds.addAll(account.findCurrentTransactionIdsWithPivotDateConfig()); - existingReversedTransactionIds.addAll(account.findCurrentReversedTransactionIdsWithPivotDateConfig()); - } - - private void postJournalEntries(final SavingsAccount savingsAccount, final Set existingTransactionIds, - final Set existingReversedTransactionIds, final boolean backdatedTxnsAllowedTill) { - - boolean isAccountTransfer = false; - final Map accountingBridgeData = savingsAccount.deriveAccountingBridgeData(savingsAccount.getCurrency().getCode(), - existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill); - this.journalEntryWritePlatformService.createJournalEntriesForSavings(accountingBridgeData); - } - - @Override - public CommandProcessingResult inactivateCharge(final Long savingsAccountId, final Long savingsAccountChargeId) { - - this.context.authenticatedUser(); - - final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository - .findOneWithNotFoundDetection(savingsAccountChargeId, savingsAccountId); - - final SavingsAccount account = savingsAccountCharge.savingsAccount(); - this.savingAccountAssembler.assignSavingAccountHelpers(account); - - final LocalDate inactivationOnDate = DateUtils.getBusinessLocalDate(); - - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(SAVINGS_ACCOUNT_CHARGE_RESOURCE_NAME); - - // Only recurring fees are allowed to inactivate - if (!savingsAccountCharge.isRecurringFee()) { - baseDataValidator.reset().parameter(null).value(savingsAccountCharge.getId()) - .failWithCodeNoParameterAddedToErrorCode("charge.inactivation.allowed.only.for.recurring.charges"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - - } else { - final LocalDate nextDueDate = savingsAccountCharge.getNextDueDateFrom(inactivationOnDate); - - if (savingsAccountCharge.isChargeIsDue(nextDueDate)) { - baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("inactivation.of.charge.not.allowed.when.charge.is.due"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } else if (savingsAccountCharge.isChargeIsOverPaid(nextDueDate)) { - - final List chargePayments = new ArrayList<>(); - SavingsAccountCharge updatedCharge = savingsAccountCharge; - do { - chargePayments.clear(); - for (SavingsAccountTransaction transaction : account.getTransactions()) { - if (transaction.isPayCharge() && transaction.isNotReversed() - && transaction.isPaymentForCurrentCharge(savingsAccountCharge)) { - chargePayments.add(transaction); - } - } - // Reverse the excess payments of charge transactions - SavingsAccountTransaction lastChargePayment = getLastChargePayment(chargePayments); - this.undoTransaction(savingsAccountCharge.savingsAccount().getId(), lastChargePayment.getId(), false); - updatedCharge = account.getUpdatedChargeDetails(savingsAccountCharge); - } while (updatedCharge.isChargeIsOverPaid(nextDueDate)); - } - account.inactivateCharge(savingsAccountCharge, inactivationOnDate); - } - - return new CommandProcessingResultBuilder() // - .withEntityId(savingsAccountCharge.getId()) // - .withOfficeId(savingsAccountCharge.savingsAccount().officeId()) // - .withClientId(savingsAccountCharge.savingsAccount().clientId()) // - .withGroupId(savingsAccountCharge.savingsAccount().groupId()) // - .withSavingsId(savingsAccountCharge.savingsAccount().getId()) // - .build(); - } - - private SavingsAccountTransaction getLastChargePayment(final List chargePayments) { - if (!CollectionUtils.isEmpty(chargePayments)) { - return chargePayments.get(chargePayments.size() - 1); - } - return null; - } - - @Transactional - @Override - public CommandProcessingResult assignFieldOfficer(Long savingsAccountId, JsonCommand command) { - this.context.authenticatedUser(); - final Map actualChanges = new LinkedHashMap<>(5); - - Staff fromSavingsOfficer = null; - Staff toSavingsOfficer = null; - this.fromApiJsonDeserializer.validateForAssignSavingsOfficer(command.json()); - - final SavingsAccount savingsForUpdate = this.savingAccountRepositoryWrapper.findOneWithNotFoundDetection(savingsAccountId); - final Long fromSavingsOfficerId = command.longValueOfParameterNamed("fromSavingsOfficerId"); - final Long toSavingsOfficerId = command.longValueOfParameterNamed("toSavingsOfficerId"); - final LocalDate dateOfSavingsOfficerAssignment = command.localDateValueOfParameterNamed("assignmentDate"); - - if (fromSavingsOfficerId != null) { - fromSavingsOfficer = this.staffRepository.findByOfficeHierarchyWithNotFoundDetection(fromSavingsOfficerId, - savingsForUpdate.office().getHierarchy()); - } - if (toSavingsOfficerId != null) { - toSavingsOfficer = this.staffRepository.findByOfficeHierarchyWithNotFoundDetection(toSavingsOfficerId, - savingsForUpdate.office().getHierarchy()); - actualChanges.put("toSavingsOfficerId", toSavingsOfficer.getId()); - } - if (!savingsForUpdate.hasSavingsOfficer(fromSavingsOfficer)) { - throw new SavingsOfficerAssignmentException(savingsAccountId, fromSavingsOfficerId); - } - - savingsForUpdate.reassignSavingsOfficer(toSavingsOfficer, dateOfSavingsOfficerAssignment); - - this.savingAccountRepositoryWrapper.saveAndFlush(savingsForUpdate); - - return new CommandProcessingResultBuilder() // - .withCommandId(command.commandId()) // - .withOfficeId(savingsForUpdate.officeId()) // - .withEntityId(savingsForUpdate.getId()) // - .withSavingsId(savingsAccountId) // - .with(actualChanges) // - .build(); - } - - @Transactional - @Override - public CommandProcessingResult unassignFieldOfficer(Long savingsAccountId, JsonCommand command) { - - this.context.authenticatedUser(); - - final Map actualChanges = new LinkedHashMap<>(5); - this.fromApiJsonDeserializer.validateForUnAssignSavingsOfficer(command.json()); - - final SavingsAccount savingsForUpdate = this.savingAccountRepositoryWrapper.findOneWithNotFoundDetection(savingsAccountId); - if (savingsForUpdate.getSavingsOfficer() == null) { - throw new SavingsOfficerUnassignmentException(savingsAccountId); - } - - final LocalDate dateOfSavingsOfficerUnassigned = command.localDateValueOfParameterNamed("unassignedDate"); - - savingsForUpdate.removeSavingsOfficer(dateOfSavingsOfficerUnassigned); - - this.savingAccountRepositoryWrapper.saveAndFlush(savingsForUpdate); - - actualChanges.put("toSavingsOfficerId", null); - - return new CommandProcessingResultBuilder() // - .withCommandId(command.commandId()) // - .withOfficeId(savingsForUpdate.officeId()) // - .withEntityId(savingsForUpdate.getId()) // - .withSavingsId(savingsAccountId) // - .with(actualChanges) // - .build(); - } - - @Override - public CommandProcessingResult modifyWithHoldTax(Long savingsAccountId, JsonCommand command) { - final Map actualChanges = new HashMap<>(1); - final SavingsAccount savingsForUpdate = this.savingAccountRepositoryWrapper.findOneWithNotFoundDetection(savingsAccountId); - if (command.isChangeInBooleanParameterNamed(withHoldTaxParamName, savingsForUpdate.withHoldTax())) { - final boolean newValue = command.booleanPrimitiveValueOfParameterNamed(withHoldTaxParamName); - actualChanges.put(withHoldTaxParamName, newValue); - savingsForUpdate.setWithHoldTax(newValue); - if (savingsForUpdate.getTaxGroup() == null) { - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("account"); - baseDataValidator.reset().parameter(withHoldTaxParamName).failWithCode("not.supported"); - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - return new CommandProcessingResultBuilder() // - .withCommandId(command.commandId()) // - .withEntityId(savingsAccountId) // - .withSavingsId(savingsAccountId) // - .with(actualChanges) // - .build(); - } - - @Override - public void setSubStatusInactive(Long savingsId) { - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); - final Set existingTransactionIds = new HashSet<>(); - final Set existingReversedTransactionIds = new HashSet<>(); - updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); - account.setSubStatusInactive(false); - this.savingAccountRepositoryWrapper.saveAndFlush(account); - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false); - } - - @Override - public void setSubStatusDormant(Long savingsId) { - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); - account.setSubStatusDormant(); - this.savingAccountRepositoryWrapper.saveAndFlush(account); - } - - @Override - public void escheat(Long savingsId) { - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); - final Set existingTransactionIds = new HashSet<>(); - final Set existingReversedTransactionIds = new HashSet<>(); - updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); - account.escheat(appuserRepository.fetchSystemUser()); - this.savingAccountRepositoryWrapper.saveAndFlush(account); - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false); - } - - private AppUser getAppUserIfPresent() { - AppUser user = null; - if (this.context != null) { - user = this.context.getAuthenticatedUserIfPresent(); - } - return user; - } - - /** - * Disable all standing instructions linked to the savings account if the status is "closed" - * - * @param savingsAccount - * -- the savings account object - * - **/ - @Transactional - private void disableStandingInstructionsLinkedToClosedSavings(final SavingsAccount savingsAccount) { - if (savingsAccount != null && savingsAccount.isClosed()) { - final Integer standingInstructionStatus = StandingInstructionStatus.ACTIVE.getValue(); - final Collection accountTransferStandingInstructions = this.standingInstructionRepository - .findBySavingsAccountAndStatus(savingsAccount, standingInstructionStatus); - - if (!accountTransferStandingInstructions.isEmpty()) { - for (AccountTransferStandingInstruction accountTransferStandingInstruction : accountTransferStandingInstructions) { - accountTransferStandingInstruction.updateStatus(StandingInstructionStatus.DISABLED.getValue()); - this.standingInstructionRepository.save(accountTransferStandingInstruction); - } - } - } - } - - @Override - public CommandProcessingResult blockAccount(final Long savingsId, final JsonCommand command) { - - this.context.authenticatedUser(); - - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); - checkClientOrGroupActive(account); - - final Map changes = account.block(); - - final String reasonForBlock = command.stringValueOfParameterNamed(SavingsApiConstants.reasonForBlockParamName); - validateReasonForHold(reasonForBlock); - account.updateReason(reasonForBlock); - - if (!changes.isEmpty()) { - - this.savingAccountRepositoryWrapper.save(account); - } - return new CommandProcessingResultBuilder().withEntityId(savingsId).withOfficeId(account.officeId()) - .withClientId(account.clientId()).withGroupId(account.groupId()).withSavingsId(savingsId).with(changes).build(); - } - - @Override - public CommandProcessingResult unblockAccount(final Long savingsId) { - this.context.authenticatedUser(); - - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); - checkClientOrGroupActive(account); - - final Map changes = account.unblock(); - - account.updateReason(null); - - if (!changes.isEmpty()) { - - this.savingAccountRepositoryWrapper.save(account); - } - return new CommandProcessingResultBuilder().withEntityId(savingsId).withOfficeId(account.officeId()) - .withClientId(account.clientId()).withGroupId(account.groupId()).withSavingsId(savingsId).with(changes).build(); - } - - @Transactional - @Override - public CommandProcessingResult holdAmount(final Long savingsId, final JsonCommand command) { - - final AppUser submittedBy = this.context.authenticatedUser(); - final boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus(); - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, backdatedTxnsAllowedTill); - final LocalDate transactionDate = command.localDateValueOfParameterNamed(transactionDateParamName); - final boolean lienAllowed = command.booleanPrimitiveValueOfParameterNamed(lienAllowedParamName); - - checkClientOrGroupActive(account); - - final BigDecimal amount = command.bigDecimalValueOfParameterNamed(transactionAmountParamName); - - Money runningBalance = Money.of(account.getCurrency(), account.getAccountBalance()); - if (account.getSavingsHoldAmount() != null) { - runningBalance = runningBalance.minus(account.getSavingsHoldAmount()).minus(amount); - } else { - runningBalance = runningBalance.minus(amount); - } - - this.savingsAccountTransactionDataValidator.validateHoldAndAssembleForm(command.json(), account, submittedBy, - backdatedTxnsAllowedTill); - SavingsAccountTransaction transaction = this.savingsAccountDomainService.handleHold(account, amount, transactionDate, lienAllowed); - account.holdAmount(amount); - transaction.setRunningBalance(runningBalance); - - final String reasonForBlock = command.stringValueOfParameterNamed(SavingsApiConstants.reasonForBlockParamName); - transaction.updateReason(reasonForBlock); - - account.getAccountBalance(); - this.savingsAccountTransactionDataValidator.validateTransactionWithPivotDate(transaction.getTransactionDate(), account); - - this.savingsAccountTransactionRepository.saveAndFlush(transaction); - - if (backdatedTxnsAllowedTill) { - // Check again whether transactions are modified - this.savingsAccountTransactionRepository.saveAll(account.getSavingsAccountTransactionsWithPivotConfig()); - } - - this.savingAccountRepositoryWrapper.saveAndFlush(account); - - return new CommandProcessingResultBuilder().withEntityId(transaction.getId()).withOfficeId(account.officeId()) - .withClientId(account.clientId()).withGroupId(account.groupId()).withSavingsId(savingsId).build(); - } - - @Transactional - @Override - public CommandProcessingResult releaseAmount(final Long savingsId, final Long savingsTransactionId) { - context.authenticatedUser(); - SavingsAccountTransaction holdTransaction = this.savingsAccountTransactionRepository - .findOneByIdAndSavingsAccountId(savingsTransactionId, savingsId); - - holdTransaction.updateReason(null); - - final SavingsAccountTransaction transaction = this.savingsAccountTransactionDataValidator - .validateReleaseAmountAndAssembleForm(holdTransaction); - - final boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus(); - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, backdatedTxnsAllowedTill); - checkClientOrGroupActive(account); - - Money runningBalance = Money.of(account.getCurrency(), account.getAccountBalance()); - - Money savingsOnHold = Money.of(account.getCurrency(), account.getSavingsHoldAmount()); - - runningBalance = runningBalance.minus(savingsOnHold); - - runningBalance = runningBalance.plus(transaction.getAmount()); - transaction.setRunningBalance(runningBalance); - - this.savingsAccountTransactionDataValidator.validateTransactionWithPivotDate(transaction.getTransactionDate(), account); - account.releaseOnHoldAmount(transaction.getAmount()); - - this.savingsAccountTransactionRepository.saveAndFlush(transaction); - holdTransaction.updateReleaseId(transaction.getId()); - - if (backdatedTxnsAllowedTill) { - this.savingsAccountTransactionRepository.saveAll(account.getSavingsAccountTransactionsWithPivotConfig()); - } else { - account.addTransaction(transaction); - } - - this.savingAccountRepositoryWrapper.save(account); - - return new CommandProcessingResultBuilder().withEntityId(transaction.getId()).withOfficeId(account.officeId()) - .withClientId(account.clientId()).withGroupId(account.groupId()).withSavingsId(account.getId()).build(); - } - - @Transactional - @Override - public CommandProcessingResult blockCredits(final Long savingsId, final JsonCommand command) { - this.context.authenticatedUser(); - - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); - checkClientOrGroupActive(account); - - final String reasonForBlock = command.stringValueOfParameterNamed(SavingsApiConstants.reasonForBlockParamName); - validateReasonForHold(reasonForBlock); - account.updateReason(reasonForBlock); - - final Map changes = account.blockCredits(account.getSubStatus()); - if (!changes.isEmpty()) { - - this.savingAccountRepositoryWrapper.save(account); - } - return new CommandProcessingResultBuilder().withEntityId(savingsId).withOfficeId(account.officeId()) - .withClientId(account.clientId()).withGroupId(account.groupId()).withSavingsId(savingsId).with(changes).build(); - } - - @Transactional - @Override - public CommandProcessingResult unblockCredits(final Long savingsId) { - this.context.authenticatedUser(); - - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); - checkClientOrGroupActive(account); - account.updateReason(null); - final Map changes = account.unblockCredits(); - if (!changes.isEmpty()) { - - this.savingAccountRepositoryWrapper.save(account); - } - return new CommandProcessingResultBuilder().withEntityId(savingsId).withOfficeId(account.officeId()) - .withClientId(account.clientId()).withGroupId(account.groupId()).withSavingsId(savingsId).with(changes).build(); - } - - @Transactional - @Override - public CommandProcessingResult blockDebits(final Long savingsId, final JsonCommand command) { - this.context.authenticatedUser(); - - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); - checkClientOrGroupActive(account); - - final String reasonForBlock = command.stringValueOfParameterNamed(SavingsApiConstants.reasonForBlockParamName); - validateReasonForHold(reasonForBlock); - account.updateReason(reasonForBlock); - - final Map changes = account.blockDebits(account.getSubStatus()); - if (!changes.isEmpty()) { - - this.savingAccountRepositoryWrapper.save(account); - } - return new CommandProcessingResultBuilder().withEntityId(savingsId).withOfficeId(account.officeId()) - .withClientId(account.clientId()).withGroupId(account.groupId()).withSavingsId(savingsId).with(changes).build(); - } - - @Transactional - @Override - public CommandProcessingResult unblockDebits(final Long savingsId) { - this.context.authenticatedUser(); - - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); - checkClientOrGroupActive(account); - - account.updateReason(null); - - final Map changes = account.unblockDebits(); - if (!changes.isEmpty()) { - - this.savingAccountRepositoryWrapper.save(account); - } - return new CommandProcessingResultBuilder().withEntityId(savingsId).withOfficeId(account.officeId()) - .withClientId(account.clientId()).withGroupId(account.groupId()).withSavingsId(savingsId).with(changes).build(); - } - - private void validateTransactionsForTransfer(final SavingsAccount savingsAccount, final LocalDate transferDate) { - for (SavingsAccountTransaction transaction : savingsAccount.getTransactions()) { - if ((DateUtils.isEqual(transferDate, transaction.getTransactionDate()) - && DateUtils.isEqual(transferDate, transaction.getSubmittedOnDate())) - || DateUtils.isBefore(transferDate, transaction.getTransactionDate())) { - throw new GeneralPlatformDomainRuleException(TransferApiConstants.transferClientSavingsException, - TransferApiConstants.transferClientSavingsException, transaction.getTransactionDate(), transferDate); - } - } - } - - private void validateReasonForHold(String reasonForBlock) { - if (StringUtils.isBlank(reasonForBlock)) { - throw new PlatformDataIntegrityException("Reason For Block is Mandatory", "error.msg.reason.for.block.mandatory"); - } - } -} +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.savings.service; + +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.SAVINGS_ACCOUNT_CHARGE_RESOURCE_NAME; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.amountParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.chargeIdParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.dueAsOfDateParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.lienAllowedParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.transactionAmountParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.transactionDateParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.withHoldTaxParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.withdrawBalanceParamName; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import io.github.resilience4j.retry.annotation.Retry; +import java.math.BigDecimal; +import java.math.MathContext; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +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 java.util.UUID; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService; +import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.ApiParameterError; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; +import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; +import org.apache.fineract.infrastructure.core.exception.ErrorHandler; +import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException; +import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; +import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; +import org.apache.fineract.infrastructure.core.exception.PlatformServiceUnavailableException; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.MathUtil; +import org.apache.fineract.infrastructure.dataqueries.data.EntityTables; +import org.apache.fineract.infrastructure.dataqueries.data.StatusEnum; +import org.apache.fineract.infrastructure.dataqueries.service.EntityDatatableChecksWritePlatformService; +import org.apache.fineract.infrastructure.event.business.domain.savings.SavingsActivateBusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.savings.SavingsCloseBusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.savings.SavingsPostInterestBusinessEvent; +import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.organisation.holiday.domain.HolidayRepositoryWrapper; +import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.organisation.monetary.domain.MoneyHelper; +import org.apache.fineract.organisation.office.domain.Office; +import org.apache.fineract.organisation.staff.domain.Staff; +import org.apache.fineract.organisation.staff.domain.StaffRepositoryWrapper; +import org.apache.fineract.organisation.workingdays.domain.WorkingDaysRepositoryWrapper; +import org.apache.fineract.portfolio.account.PortfolioAccountType; +import org.apache.fineract.portfolio.account.domain.AccountTransferStandingInstruction; +import org.apache.fineract.portfolio.account.domain.StandingInstructionRepository; +import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus; +import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlatformService; +import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService; +import org.apache.fineract.portfolio.charge.domain.Charge; +import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper; +import org.apache.fineract.portfolio.denominationmaster.domain.SavingsTxnDenomination; +import org.apache.fineract.portfolio.denominationmaster.domain.SavingsTxnDenominationRepository; +import org.apache.fineract.portfolio.charge.domain.ChargeTimeType; +import org.apache.fineract.portfolio.client.domain.Client; +import org.apache.fineract.portfolio.client.exception.ClientNotActiveException; +import org.apache.fineract.portfolio.group.domain.Group; +import org.apache.fineract.portfolio.group.exception.GroupNotActiveException; +import org.apache.fineract.portfolio.note.domain.Note; +import org.apache.fineract.portfolio.note.domain.NoteRepository; +import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail; +import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService; +import org.apache.fineract.portfolio.savings.service.FacilityChargeAutoApplyService; +import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType; +import org.apache.fineract.portfolio.savings.SavingsApiConstants; +import org.apache.fineract.portfolio.savings.SavingsTransactionBooleanValues; +import org.apache.fineract.portfolio.savings.data.SavingsAccountChargeDataValidator; +import org.apache.fineract.portfolio.savings.data.SavingsAccountData; +import org.apache.fineract.portfolio.savings.data.SavingsAccountDataValidator; +import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionDTO; +import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData; +import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionDataValidator; +import org.apache.fineract.portfolio.savings.domain.DepositAccountOnHoldTransaction; +import org.apache.fineract.portfolio.savings.domain.DepositAccountOnHoldTransactionRepository; +import org.apache.fineract.portfolio.savings.domain.GSIMRepositoy; +import org.apache.fineract.portfolio.savings.domain.GroupSavingsIndividualMonitoring; +import org.apache.fineract.portfolio.savings.domain.SavingsAccount; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountCharge; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountChargeRepositoryWrapper; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountStatusType; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransactionRepository; +import org.apache.fineract.portfolio.savings.exception.PostInterestAsOnDateException; +import org.apache.fineract.portfolio.savings.exception.PostInterestAsOnDateException.PostInterestAsOnExceptionType; +import org.apache.fineract.portfolio.savings.exception.PostInterestClosingDateException; +import org.apache.fineract.portfolio.savings.exception.SavingsAccountClosingNotAllowedException; +import org.apache.fineract.portfolio.savings.exception.SavingsAccountTransactionNotFoundException; +import org.apache.fineract.portfolio.savings.exception.SavingsOfficerAssignmentException; +import org.apache.fineract.portfolio.savings.exception.SavingsOfficerUnassignmentException; +import org.apache.fineract.portfolio.savings.exception.TransactionUpdateNotAllowedException; +import org.apache.fineract.portfolio.transfer.api.TransferApiConstants; +import org.apache.fineract.useradministration.domain.AppUser; +import org.apache.fineract.useradministration.domain.AppUserRepositoryWrapper; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; + +@Slf4j +@RequiredArgsConstructor +public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements SavingsAccountWritePlatformService { + + private final PlatformSecurityContext context; + private final SavingsAccountDataValidator fromApiJsonDeserializer; + private final SavingsAccountRepositoryWrapper savingAccountRepositoryWrapper; + private final StaffRepositoryWrapper staffRepository; + private final SavingsAccountTransactionRepository savingsAccountTransactionRepository; + private final SavingsAccountAssembler savingAccountAssembler; + private final SavingsAccountTransactionDataValidator savingsAccountTransactionDataValidator; + private final SavingsAccountChargeDataValidator savingsAccountChargeDataValidator; + private final PaymentDetailWritePlatformService paymentDetailWritePlatformService; + private final JournalEntryWritePlatformService journalEntryWritePlatformService; + private final SavingsAccountDomainService savingsAccountDomainService; + private final NoteRepository noteRepository; + private final AccountTransfersReadPlatformService accountTransfersReadPlatformService; + private final AccountAssociationsReadPlatformService accountAssociationsReadPlatformService; + private final ChargeRepositoryWrapper chargeRepository; + private final SavingsAccountChargeRepositoryWrapper savingsAccountChargeRepository; + private final HolidayRepositoryWrapper holidayRepository; + private final WorkingDaysRepositoryWrapper workingDaysRepository; + private final ConfigurationDomainService configurationDomainService; + private final DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository; + private final EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService; + private final AppUserRepositoryWrapper appuserRepository; + private final StandingInstructionRepository standingInstructionRepository; + private final BusinessEventNotifierService businessEventNotifierService; + private final GSIMRepositoy gsimRepository; + private final SavingsAccountInterestPostingService savingsAccountInterestPostingService; + private final ErrorHandler errorHandler; + private final FacilityChargeAutoApplyService facilityChargeAutoApplyService; + private final SavingsTxnDenominationRepository savingsTxnDenominationRepository; + + @Transactional + @Override + public CommandProcessingResult gsimActivate(final Long gsimId, final JsonCommand command) { + + Long parentSavingId = gsimId; + GroupSavingsIndividualMonitoring parentSavings = gsimRepository.findById(parentSavingId).orElseThrow(); + List childSavings = this.savingAccountRepositoryWrapper.findByGsimId(gsimId); + + CommandProcessingResult result = null; + int count = 0; + for (SavingsAccount account : childSavings) { + result = activate(account.getId(), command); + if (result != null) { + count++; + if (count == parentSavings.getChildAccountsCount()) { + parentSavings.setSavingsStatus(SavingsAccountStatusType.ACTIVE.getValue()); + gsimRepository.save(parentSavings); + } + } + } + return result; + } + + @Transactional + @Override + public CommandProcessingResult activate(final Long savingsId, final JsonCommand command) { + + final AppUser user = this.context.authenticatedUser(); + + this.savingsAccountTransactionDataValidator.validateActivation(command); + + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); + + // Validate minimum opening balance + BigDecimal openingBalanceForValidation = BigDecimal.ZERO; + if (command.parameterExists("openingBalance")) { + final BigDecimal ob = command.bigDecimalValueOfParameterNamed("openingBalance"); + if (ob != null) openingBalanceForValidation = ob; + } + final BigDecimal minRequiredOpening = account.getMinRequiredOpeningBalance(); + if (minRequiredOpening != null && minRequiredOpening.compareTo(BigDecimal.ZERO) > 0 + && openingBalanceForValidation.compareTo(minRequiredOpening) < 0) { + final List errors = new ArrayList<>(); + errors.add(ApiParameterError.parameterError( + "openingBalance", + "Opening balance (" + openingBalanceForValidation + ") is less than minimum required opening balance (" + minRequiredOpening + ")", + "validation.msg.savingsaccount.openingBalance.less.than.min.required", + openingBalanceForValidation, minRequiredOpening)); + throw new PlatformApiDataValidationException(errors); + } + + checkClientOrGroupActive(account); + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + + updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + + final Map changes = account.activate(user, command); + + entityDatatableChecksWritePlatformService.runTheCheckForProduct(savingsId, EntityTables.SAVINGS.getName(), + StatusEnum.ACTIVATE.getValue(), EntityTables.SAVINGS.getForeignKeyColumnNameOnDatatable(), account.productId()); + + if (!changes.isEmpty()) { + final Locale locale = command.extractLocale(); + final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); + processPostActiveActions(account, fmt, existingTransactionIds, existingReversedTransactionIds); + // Auto-apply facility charges based on product facility config + facilityChargeAutoApplyService.applyFacilityChargesOnActivation(account); + this.savingAccountRepositoryWrapper.saveAndFlush(account); + // Collect any newly added facility activation charges (after flush so amountOutstanding is set) + // payCharge() internally adds transactions to account.transactions via handleChargeTransactions + // Do NOT add to existingTransactionIds — postJournalEntries must include them for GL posting + facilityChargeAutoApplyService.collectActivationCharges(account); + this.savingAccountRepositoryWrapper.saveAndFlush(account); + } + + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false); + businessEventNotifierService.notifyPostBusinessEvent(new SavingsActivateBusinessEvent(account)); + + return new CommandProcessingResultBuilder() // + .withEntityId(savingsId) // + .withOfficeId(account.officeId()) // + .withClientId(account.clientId()) // + .withGroupId(account.groupId()) // + .withSavingsId(savingsId) // + .with(changes) // + .build(); + } + + @Override + public void processPostActiveActions(final SavingsAccount account, final DateTimeFormatter fmt, final Set existingTransactionIds, + final Set existingReversedTransactionIds) { + getAppUserIfPresent(); + + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + + Money amountForDeposit = account.activateWithBalance(); + boolean isRegularTransaction = false; + if (amountForDeposit.isGreaterThanZero()) { + boolean isAccountTransfer = false; + this.savingsAccountDomainService.handleDeposit(account, fmt, account.getActivationDate(), amountForDeposit.getAmount(), null, + isAccountTransfer, isRegularTransaction, false); + + updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + } + + account.processAccountUponActivation(isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth); + List depositAccountOnHoldTransactions = null; + if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { + depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository + .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); + } + + account.validateAccountBalanceDoesNotBecomeNegative(SavingsAccountTransactionType.PAY_CHARGE.name(), + depositAccountOnHoldTransactions, false); + } + + @Transactional + @Override + public CommandProcessingResult gsimDeposit(final Long gsimId, final JsonCommand command) { + JsonArray savingsArray = command.arrayOfParameterNamed("savingsArray"); + + CommandProcessingResult result = null; + for (JsonElement element : savingsArray) { + result = deposit(element.getAsJsonObject().get("childAccountId").getAsLong(), + JsonCommand.fromExistingCommand(command, element)); + } + return result; + } + + @Transactional + @Override + public CommandProcessingResult deposit(final Long savingsId, final JsonCommand command) { + this.context.authenticatedUser(); + + this.savingsAccountTransactionDataValidator.validate(command); + boolean isGsim = false; + + final boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus(); + + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, backdatedTxnsAllowedTill); + + if (account.getGsim() != null) { + isGsim = true; + log.debug("is gsim"); + } + checkClientOrGroupActive(account); + + final Locale locale = command.extractLocale(); + final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); + + final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); + final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount"); + + this.savingsAccountTransactionDataValidator.validateTransactionWithPivotDate(transactionDate, account); + + final Map changes = new LinkedHashMap<>(); + final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); + boolean isAccountTransfer = false; + boolean isRegularTransaction = true; + final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(account, fmt, transactionDate, + transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill); + + // Save denomination breakdown if provided + if (command.parameterExists("denominationBreakdown")) { + final JsonArray denominationBreakdown = command.arrayOfParameterNamed("denominationBreakdown"); + if (denominationBreakdown != null && denominationBreakdown.size() > 0) { + final Long clientId = account.clientId(); + final String accountNo = account.getAccountNumber(); + final String cifNo = account.getExternalId() != null ? account.getExternalId().getValue() : null; + for (JsonElement entry : denominationBreakdown) { + final com.google.gson.JsonObject obj = entry.getAsJsonObject(); + if (!obj.has("denominationId") || !obj.has("count")) continue; + final long denominationId = obj.get("denominationId").getAsLong(); + final int count = obj.get("count").getAsInt(); + if (count <= 0) continue; + final int denominationValue = obj.has("denominationValue") ? obj.get("denominationValue").getAsInt() : 0; + final BigDecimal total = BigDecimal.valueOf((long) denominationValue * count); + final SavingsTxnDenomination row = SavingsTxnDenomination.builder() + .txnId(deposit.getId()) + .savingsAccountId(account.getId()) + .accountNo(accountNo) + .clientId(clientId) + .cifNo(cifNo) + .denominationId(denominationId) + .denominationValue(denominationValue) + .count(count) + .totalAmount(total) + .txnDate(transactionDate) + .createdAt(LocalDateTime.now()) + .build(); + this.savingsTxnDenominationRepository.save(row); + } + } + } + + if (isGsim && (deposit.getId() != null)) { + + log.debug("Deposit account has been created: {} ", deposit); + + GroupSavingsIndividualMonitoring gsim = gsimRepository.findById(account.getGsim().getId()).orElseThrow(); + log.debug("parent deposit : {} ", gsim.getParentDeposit()); + log.debug("child account : {} ", savingsId); + BigDecimal currentBalance = gsim.getParentDeposit(); + BigDecimal newBalance = currentBalance.add(transactionAmount); + gsim.setParentDeposit(newBalance); + gsimRepository.save(gsim); + log.debug("balance after making deposit : {} ", + gsimRepository.findById(account.getGsim().getId()).orElseThrow().getParentDeposit()); + + } + + final String noteText = command.stringValueOfParameterNamed("note"); + if (StringUtils.isNotBlank(noteText)) { + final Note note = Note.savingsTransactionNote(account, deposit, noteText); + this.noteRepository.save(note); + } + + return new CommandProcessingResultBuilder() // + .withEntityId(deposit.getId()) // + .withOfficeId(account.officeId()) // + .withClientId(account.clientId()) // + .withGroupId(account.groupId()) // + .withSavingsId(savingsId) // + .with(changes) // + .build(); + } + + private Long saveTransactionToGenerateTransactionId(final SavingsAccountTransaction transaction) { + this.savingsAccountTransactionRepository.saveAndFlush(transaction); + return transaction.getId(); + } + + @Transactional + @Override + public CommandProcessingResult withdrawal(final Long savingsId, final JsonCommand command) { + + this.savingsAccountTransactionDataValidator.validate(command); + + boolean isGsim = false; + + final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); + final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount"); + + final Locale locale = command.extractLocale(); + final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); + + final Map changes = new LinkedHashMap<>(); + final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); + + final boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus(); + + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, backdatedTxnsAllowedTill); + + if (account.getGsim() != null) { + isGsim = true; + } + checkClientOrGroupActive(account); + + this.savingsAccountTransactionDataValidator.validateTransactionWithPivotDate(transactionDate, account); + + final boolean isAccountTransfer = false; + final boolean isRegularTransaction = true; + final boolean isApplyWithdrawFee = true; + final boolean isInterestTransfer = false; + final boolean isWithdrawBalance = false; + final SavingsTransactionBooleanValues transactionBooleanValues = new SavingsTransactionBooleanValues(isAccountTransfer, + isRegularTransaction, isApplyWithdrawFee, isInterestTransfer, isWithdrawBalance); + final SavingsAccountTransaction withdrawal = this.savingsAccountDomainService.handleWithdrawal(account, fmt, transactionDate, + transactionAmount, paymentDetail, transactionBooleanValues, backdatedTxnsAllowedTill); + + if (isGsim && (withdrawal.getId() != null)) { + GroupSavingsIndividualMonitoring gsim = gsimRepository.findById(account.getGsim().getId()).orElseThrow(); + BigDecimal currentBalance = gsim.getParentDeposit().subtract(transactionAmount); + gsim.setParentDeposit(currentBalance); + gsimRepository.save(gsim); + + } + + final String noteText = command.stringValueOfParameterNamed("note"); + if (StringUtils.isNotBlank(noteText)) { + final Note note = Note.savingsTransactionNote(account, withdrawal, noteText); + this.noteRepository.save(note); + } + + return new CommandProcessingResultBuilder() // + .withEntityId(withdrawal.getId()) // + .withOfficeId(account.officeId()) // + .withClientId(account.clientId()) // + .withGroupId(account.groupId()) // + .withSavingsId(savingsId) // + .with(changes)// + .build(); + } + + @Transactional + @Override + public CommandProcessingResult applyAnnualFee(final Long savingsAccountChargeId, final Long accountId) { + getAppUserIfPresent(); + + final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository + .findOneWithNotFoundDetection(savingsAccountChargeId, accountId); + + final LocalDate currentDate = DateUtils.getBusinessLocalDate(); + final DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd MM yyyy").withZone(DateUtils.getDateTimeZoneOfTenant()); + + while (DateUtils.isBefore(savingsAccountCharge.getDueDate(), currentDate)) { + this.payCharge(savingsAccountCharge, savingsAccountCharge.getDueDate(), savingsAccountCharge.amount(), fmt, false); + } + + return new CommandProcessingResultBuilder() // + .withEntityId(savingsAccountCharge.getId()) // + .withOfficeId(savingsAccountCharge.savingsAccount().officeId()) // + .withClientId(savingsAccountCharge.savingsAccount().clientId()) // + .withGroupId(savingsAccountCharge.savingsAccount().groupId()) // + .withSavingsId(savingsAccountCharge.savingsAccount().getId()) // + .build(); + } + + @Transactional + @Override + public CommandProcessingResult calculateInterest(final Long savingsId) { + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + + final boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus(); + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, backdatedTxnsAllowedTill); + checkClientOrGroupActive(account); + + final LocalDate today = DateUtils.getBusinessLocalDate(); + final MathContext mc = new MathContext(15, MoneyHelper.getRoundingMode()); + boolean isInterestTransfer = false; + final LocalDate postInterestOnDate = null; + boolean postReversals = false; + account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals); + + if (backdatedTxnsAllowedTill) { + this.savingsAccountTransactionRepository.saveAll(account.getSavingsAccountTransactionsWithPivotConfig()); + } + + this.savingAccountRepositoryWrapper.save(account); + + return new CommandProcessingResultBuilder() // + .withEntityId(savingsId) // + .withOfficeId(account.officeId()) // + .withClientId(account.clientId()) // + .withGroupId(account.groupId()) // + .withSavingsId(savingsId) // + .build(); + } + + @Override + @Transactional + public CommandProcessingResult postInterest(final JsonCommand command) { + Long savingsId = command.getSavingsId(); + final boolean postInterestAs = command.booleanPrimitiveValueOfParameterNamed("isPostInterestAsOn"); + final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); + + final boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus(); + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, backdatedTxnsAllowedTill); + checkClientOrGroupActive(account); + + this.savingsAccountTransactionDataValidator.validateTransactionWithPivotDate(transactionDate, account); + + if (postInterestAs) { + if (transactionDate == null) { + throw new PostInterestAsOnDateException(PostInterestAsOnExceptionType.VALID_DATE); + } + if (DateUtils.isBefore(transactionDate, account.accountSubmittedOrActivationDate())) { + throw new PostInterestAsOnDateException(PostInterestAsOnExceptionType.ACTIVATION_DATE); + } + + List savingTransactions = null; + if (backdatedTxnsAllowedTill) { + savingTransactions = account.getSavingsAccountTransactionsWithPivotConfig(); + } else { + savingTransactions = account.getTransactions(); + } + + for (SavingsAccountTransaction savingTransaction : savingTransactions) { + if (DateUtils.isBefore(transactionDate, savingTransaction.getDateOf())) { + throw new PostInterestAsOnDateException(PostInterestAsOnExceptionType.LAST_TRANSACTION_DATE); + } + } + + if (DateUtils.isDateInTheFuture(transactionDate)) { + throw new PostInterestAsOnDateException(PostInterestAsOnExceptionType.FUTURE_DATE); + } + } + postInterest(account, postInterestAs, transactionDate, backdatedTxnsAllowedTill); + + businessEventNotifierService.notifyPostBusinessEvent(new SavingsPostInterestBusinessEvent(account)); + return new CommandProcessingResultBuilder() // + .withEntityId(savingsId) // + .withOfficeId(account.officeId()) // + .withClientId(account.clientId()) // + .withGroupId(account.groupId()) // + .withSavingsId(savingsId) // + .build(); + } + + @Transactional + @Override + public void postInterest(final SavingsAccount account, final boolean postInterestAs, final LocalDate transactionDate, + final boolean backdatedTxnsAllowedTill) { + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + + if (account.getNominalAnnualInterestRate().compareTo(BigDecimal.ZERO) > 0 + || (account.allowOverdraft() && account.getNominalAnnualInterestRateOverdraft().compareTo(BigDecimal.ZERO) > 0)) { + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + + if (backdatedTxnsAllowedTill) { + updateSavingsTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + } else { + updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + } + + final LocalDate today = DateUtils.getBusinessLocalDate(); + final MathContext mc = new MathContext(10, MoneyHelper.getRoundingMode()); + boolean isInterestTransfer = false; + LocalDate postInterestOnDate = null; + if (postInterestAs) { + postInterestOnDate = transactionDate; + } + boolean postReversals = false; + account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, + postInterestOnDate, backdatedTxnsAllowedTill, postReversals); + + if (!backdatedTxnsAllowedTill) { + List transactions = account.getTransactions(); + for (SavingsAccountTransaction accountTransaction : transactions) { + if (accountTransaction.getId() == null) { + this.savingsAccountTransactionRepository.save(accountTransaction); + } + } + } + + if (backdatedTxnsAllowedTill) { + this.savingsAccountTransactionRepository.saveAll(account.getSavingsAccountTransactionsWithPivotConfig()); + } + + this.savingAccountRepositoryWrapper.saveAndFlush(account); + + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, backdatedTxnsAllowedTill); + } + } + + @Transactional + @Override + @Retry(name = "postInterest", fallbackMethod = "fallbackPostInterest") + public SavingsAccountData postInterest(SavingsAccountData savingsAccountData, final boolean postInterestAs, + final LocalDate transactionDate, final boolean backdatedTxnsAllowedTill) { + + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + + if (MathUtil.isGreaterThanZero(savingsAccountData.getNominalAnnualInterestRate()) || (savingsAccountData.isAllowOverdraft() + && MathUtil.isGreaterThanZero(savingsAccountData.getNominalAnnualInterestRateOverdraft()))) { + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + updateExistingTransactionsDetails(savingsAccountData, existingTransactionIds, existingReversedTransactionIds); + + final LocalDate today = DateUtils.getBusinessLocalDate(); + final MathContext mc = new MathContext(10, MoneyHelper.getRoundingMode()); + boolean isInterestTransfer = false; + LocalDate postInterestOnDate = null; + if (postInterestAs) { + postInterestOnDate = transactionDate; + } + + savingsAccountData = this.savingsAccountInterestPostingService.postInterest(mc, today, isInterestTransfer, + isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, + savingsAccountData); + + if (!backdatedTxnsAllowedTill) { + List transactions = savingsAccountData.getSavingsAccountTransactionData(); + for (SavingsAccountTransactionData accountTransaction : transactions) { + if (accountTransaction.getId() == null) { + savingsAccountData.setNewSavingsAccountTransactionData(accountTransaction); + selectAccountId(accountTransaction, savingsAccountData); + } + } + } + savingsAccountData.setExistingTransactionIds(existingTransactionIds); + savingsAccountData.setExistingReversedTransactionIds(existingReversedTransactionIds); + } + return savingsAccountData; + } + + public void selectAccountId(SavingsAccountTransactionData accountTransaction, SavingsAccountData savingsAccountData) { + SavingsAccountTransactionType transactionType = SavingsAccountTransactionType + .fromInt(accountTransaction.getTransactionType().getId().intValue()); + if (transactionType.isOverDraftInterestPosting()) { + if (MathUtil.isGreaterThanZero(accountTransaction.getRunningBalance())) { + accountTransaction.setAccountDebit(savingsAccountData.getGlAccountIdForSavingsControl()); + accountTransaction.setAccountCredit(savingsAccountData.getGlAccountIdForInterestReceivable()); + } else { + accountTransaction.setAccountDebit(savingsAccountData.getGlAccountIdForOverdraftPorfolio()); + accountTransaction.setAccountCredit(savingsAccountData.getGlAccountIdForInterestReceivable()); + } + } else { + accountTransaction.setAccountDebit(savingsAccountData.getGlAccountIdForInterestPayable()); + accountTransaction.setAccountCredit(savingsAccountData.getGlAccountIdForSavingsControl()); + } + } + + @Override + public CommandProcessingResult reverseTransaction(final Long savingsId, final Long transactionId, + final boolean allowAccountTransferModification, final JsonCommand command) { + + final boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus(); + final boolean isBulk = command.booleanPrimitiveValueOfParameterNamed("isBulk"); + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, backdatedTxnsAllowedTill); + + final SavingsAccountTransaction savingsAccountTransaction = this.savingsAccountTransactionRepository + .findOneByIdAndSavingsAccountId(transactionId, savingsId); + if (savingsAccountTransaction == null) { + throw new SavingsAccountTransactionNotFoundException(savingsId, transactionId); + } + + if (!allowAccountTransferModification + && this.accountTransfersReadPlatformService.isAccountTransfer(transactionId, PortfolioAccountType.SAVINGS)) { + throw new PlatformServiceUnavailableException("error.msg.saving.account.transfer.transaction.update.not.allowed", + "Savings account transaction:" + transactionId + " update not allowed as it involves in account transfer", + transactionId); + } + + if (!account.allowModify()) { + throw new PlatformServiceUnavailableException("error.msg.saving.account.transaction.update.not.allowed", + "Savings account transaction:" + transactionId + " update not allowed for this savings type", transactionId); + } + if (account.isNotActive()) { + throwValidationForActiveStatus(SavingsApiConstants.undoTransactionAction); + } + SavingsAccountTransaction reversal = null; + Long reversalId = null; + checkClientOrGroupActive(account); + List savingsAccountTransactions = null; + if (isBulk) { + String transactionRefNo = savingsAccountTransaction.getRefNo(); + savingsAccountTransactions = this.savingsAccountTransactionRepository.findByRefNo(transactionRefNo); + reversal = this.savingsAccountDomainService.handleReversal(account, savingsAccountTransactions, backdatedTxnsAllowedTill); + } else { + reversal = this.savingsAccountDomainService.handleReversal(account, Collections.singletonList(savingsAccountTransaction), + backdatedTxnsAllowedTill); + } + reversalId = reversal.getId(); + return new CommandProcessingResultBuilder() // + .withEntityId(reversalId) // + .withOfficeId(account.officeId()) // + .withClientId(account.clientId()) // + .withGroupId(account.groupId()) // + .withSavingsId(savingsId) // + .build(); + } + + @Override + public CommandProcessingResult undoTransaction(final Long savingsId, final Long transactionId, + final boolean allowAccountTransferModification) { + + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + + final SavingsAccountTransaction savingsAccountTransaction = this.savingsAccountTransactionRepository + .findOneByIdAndSavingsAccountId(transactionId, savingsId); + if (savingsAccountTransaction == null) { + throw new SavingsAccountTransactionNotFoundException(savingsId, transactionId); + } + + this.savingsAccountTransactionDataValidator.validateTransactionWithPivotDate(savingsAccountTransaction.getTransactionDate(), + account); + + if (!allowAccountTransferModification + && this.accountTransfersReadPlatformService.isAccountTransfer(transactionId, PortfolioAccountType.SAVINGS)) { + throw new PlatformServiceUnavailableException("error.msg.saving.account.transfer.transaction.update.not.allowed", + "Savings account transaction:" + transactionId + " update not allowed as it involves in account transfer", + transactionId); + } + + if (!account.allowModify()) { + throw new PlatformServiceUnavailableException("error.msg.saving.account.transaction.update.not.allowed", + "Savings account transaction:" + transactionId + " update not allowed for this savings type", transactionId); + } + + final LocalDate today = DateUtils.getBusinessLocalDate(); + final MathContext mc = new MathContext(15, MoneyHelper.getRoundingMode()); + + if (account.isNotActive()) { + throwValidationForActiveStatus(SavingsApiConstants.undoTransactionAction); + } + account.undoTransaction(transactionId); + + // undoing transaction is withdrawal then undo withdrawal fee + // transaction if any + if (savingsAccountTransaction.isWithdrawal()) { + final SavingsAccountTransaction nextSavingsAccountTransaction = this.savingsAccountTransactionRepository + .findOneByIdAndSavingsAccountId(transactionId + 1, savingsId); + if (nextSavingsAccountTransaction != null && nextSavingsAccountTransaction.isWithdrawalFeeAndNotReversed()) { + account.undoTransaction(transactionId + 1); + } + } + boolean isInterestTransfer = false; + LocalDate postInterestOnDate = null; + boolean postReversals = false; + checkClientOrGroupActive(account); + if (savingsAccountTransaction.isPostInterestCalculationRequired() + && account.isBeforeLastPostingPeriod(savingsAccountTransaction.getTransactionDate(), false)) { + account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, + postInterestOnDate, false, postReversals); + } else { + account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth, postInterestOnDate, false, postReversals); + } + List depositAccountOnHoldTransactions = null; + if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { + depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository + .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); + } + account.validateAccountBalanceDoesNotBecomeNegative(SavingsApiConstants.undoTransactionAction, depositAccountOnHoldTransactions, + false); + account.activateAccountBasedOnBalance(); + this.savingAccountRepositoryWrapper.saveAndFlush(account); + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false); + return new CommandProcessingResultBuilder() // + .withEntityId(savingsId) // + .withOfficeId(account.officeId()) // + .withClientId(account.clientId()) // + .withGroupId(account.groupId()) // + .withSavingsId(savingsId) // + .build(); + } + + @Override + public CommandProcessingResult adjustSavingsTransaction(final Long savingsId, final Long transactionId, final JsonCommand command) { + context.authenticatedUser(); + + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + final Long relaxingDaysConfigForPivotDate = this.configurationDomainService.retrieveRelaxingDaysConfigForPivotDate(); + final SavingsAccountTransaction savingsAccountTransaction = this.savingsAccountTransactionRepository + .findOneByIdAndSavingsAccountId(transactionId, savingsId); + if (savingsAccountTransaction == null) { + throw new SavingsAccountTransactionNotFoundException(savingsId, transactionId); + } + + if (!(savingsAccountTransaction.isDeposit() || savingsAccountTransaction.isWithdrawal()) + || savingsAccountTransaction.isReversed()) { + throw new TransactionUpdateNotAllowedException(savingsId, transactionId); + } + + if (this.accountTransfersReadPlatformService.isAccountTransfer(transactionId, PortfolioAccountType.SAVINGS)) { + throw new PlatformServiceUnavailableException("error.msg.saving.account.transfer.transaction.update.not.allowed", + "Savings account transaction:" + transactionId + " update not allowed as it involves in account transfer", + transactionId); + } + + this.savingsAccountTransactionDataValidator.validate(command); + + final LocalDate today = DateUtils.getBusinessLocalDate(); + + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); + + if (account.isNotActive()) { + throwValidationForActiveStatus(SavingsApiConstants.adjustTransactionAction); + } + if (!account.allowModify()) { + throw new PlatformServiceUnavailableException("error.msg.saving.account.transaction.update.not.allowed", + "Savings account transaction:" + transactionId + " update not allowed for this savings type", transactionId); + } + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + + updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + + final Locale locale = command.extractLocale(); + final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); + final LocalDate transactionDate = command.localDateValueOfParameterNamed(SavingsApiConstants.transactionDateParamName); + final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed(SavingsApiConstants.transactionAmountParamName); + final Map changes = new LinkedHashMap<>(); + final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); + + final MathContext mc = new MathContext(10, MoneyHelper.getRoundingMode()); + account.undoTransaction(transactionId); + + // for undo withdrawal fee + final SavingsAccountTransaction nextSavingsAccountTransaction = this.savingsAccountTransactionRepository + .findOneByIdAndSavingsAccountId(transactionId + 1, savingsId); + if (nextSavingsAccountTransaction != null && nextSavingsAccountTransaction.isWithdrawalFeeAndNotReversed()) { + account.undoTransaction(transactionId + 1); + } + + SavingsAccountTransaction transaction = null; + boolean isInterestTransfer = false; + Integer accountType = null; + final SavingsAccountTransactionDTO transactionDTO = new SavingsAccountTransactionDTO(fmt, transactionDate, transactionAmount, + paymentDetail, null, accountType); + UUID refNo = UUID.randomUUID(); + if (savingsAccountTransaction.isDeposit()) { + transaction = account.deposit(transactionDTO, false, relaxingDaysConfigForPivotDate, refNo.toString()); + } else { + transaction = account.withdraw(transactionDTO, true, false, relaxingDaysConfigForPivotDate, refNo.toString()); + } + final Long newtransactionId = saveTransactionToGenerateTransactionId(transaction); + final LocalDate postInterestOnDate = null; + boolean postReversals = false; + if (account.isBeforeLastPostingPeriod(transactionDate, false) + || account.isBeforeLastPostingPeriod(savingsAccountTransaction.getTransactionDate(), false)) { + account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, + postInterestOnDate, false, postReversals); + } else { + account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth, postInterestOnDate, false, postReversals); + } + List depositAccountOnHoldTransactions = null; + if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { + depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository + .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); + } + + account.validateAccountBalanceDoesNotBecomeNegative(SavingsApiConstants.adjustTransactionAction, depositAccountOnHoldTransactions, + false); + account.activateAccountBasedOnBalance(); + + this.savingAccountRepositoryWrapper.saveAndFlush(account); + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false); + return new CommandProcessingResultBuilder() // + .withEntityId(newtransactionId) // + .withOfficeId(account.officeId()) // + .withClientId(account.clientId()) // + .withGroupId(account.groupId()) // + .withSavingsId(savingsId) // + .with(changes)// + .build(); + } + + /** + * + */ + private void throwValidationForActiveStatus(final String actionName) { + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(SAVINGS_ACCOUNT_RESOURCE_NAME + actionName); + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("account.is.not.active"); + throw new PlatformApiDataValidationException(dataValidationErrors); + } + + private void checkClientOrGroupActive(final SavingsAccount account) { + final Client client = account.getClient(); + if (client != null) { + if (client.isNotActive()) { + throw new ClientNotActiveException(client.getId()); + } + } + final Group group = account.group(); + if (group != null) { + if (group.isNotActive()) { + throw new GroupNotActiveException(group.getId()); + } + } + } + + @Override + public CommandProcessingResult bulkGSIMClose(final Long gsimId, final JsonCommand command) { + + final Long parentSavingId = gsimId; + GroupSavingsIndividualMonitoring parentSavings = gsimRepository.findById(parentSavingId).orElseThrow(); + List childSavings = this.savingAccountRepositoryWrapper.findByGsimId(gsimId); + + CommandProcessingResult result = null; + int count = 0; + for (SavingsAccount account : childSavings) { + result = close(account.getId(), command); + + if (result != null) { + count++; + if (count == parentSavings.getChildAccountsCount()) { + parentSavings.setSavingsStatus(SavingsAccountStatusType.CLOSED.getValue()); + gsimRepository.save(parentSavings); + } + } + } + return result; + } + + @Override + public CommandProcessingResult close(final Long savingsId, final JsonCommand command) { + final AppUser user = this.context.authenticatedUser(); + + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); + this.savingsAccountTransactionDataValidator.validateClosing(command, account); + + final boolean isLinkedWithAnyActiveLoan = this.accountAssociationsReadPlatformService.isLinkedWithAnyActiveAccount(savingsId); + + if (isLinkedWithAnyActiveLoan) { + final String defaultUserMessage = "Closing savings account with id:" + savingsId + + " is not allowed, since it is linked with one of the active accounts"; + throw new SavingsAccountClosingNotAllowedException("linked", defaultUserMessage, savingsId); + } + + entityDatatableChecksWritePlatformService.runTheCheckForProduct(savingsId, EntityTables.SAVINGS.getName(), + StatusEnum.CLOSE.getValue(), EntityTables.SAVINGS.getForeignKeyColumnNameOnDatatable(), account.productId()); + + final boolean isWithdrawBalance = command.booleanPrimitiveValueOfParameterNamed(withdrawBalanceParamName); + + final Locale locale = command.extractLocale(); + final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); + final LocalDate closedDate = command.localDateValueOfParameterNamed(SavingsApiConstants.closedOnDateParamName); + final boolean isPostInterest = command.booleanPrimitiveValueOfParameterNamed(SavingsApiConstants.postInterestValidationOnClosure); + // postInterest(account,closedDate,flag); + if (isPostInterest) { + boolean postInterestOnClosingDate = false; + List savingTransactions = account.getTransactions(); + for (SavingsAccountTransaction savingTransaction : savingTransactions) { + if (savingTransaction.isInterestPosting() && savingTransaction.isNotReversed() + && DateUtils.isEqual(closedDate, savingTransaction.getTransactionDate())) { + postInterestOnClosingDate = true; + break; + } + } + if (!postInterestOnClosingDate) { + throw new PostInterestClosingDateException(); + } + } + + final Map changes = new LinkedHashMap<>(); + + if (isWithdrawBalance && account.getSummary().getAccountBalance(account.getCurrency()).isGreaterThanZero()) { + + final BigDecimal transactionAmount = account.getSummary().getAccountBalance(); + + final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); + + final boolean isAccountTransfer = false; + final boolean isRegularTransaction = true; + final boolean isApplyWithdrawFee = false; + final boolean isInterestTransfer = false; + final SavingsTransactionBooleanValues transactionBooleanValues = new SavingsTransactionBooleanValues(isAccountTransfer, + isRegularTransaction, isApplyWithdrawFee, isInterestTransfer, isWithdrawBalance); + + this.savingsAccountDomainService.handleWithdrawal(account, fmt, closedDate, transactionAmount, paymentDetail, + transactionBooleanValues, false); + + } + + final Map accountChanges = account.close(user, command); + changes.putAll(accountChanges); + if (!changes.isEmpty()) { + this.savingAccountRepositoryWrapper.save(account); + final String noteText = command.stringValueOfParameterNamed("note"); + if (StringUtils.isNotBlank(noteText)) { + final Note note = Note.savingNote(account, noteText); + changes.put("note", noteText); + this.noteRepository.save(note); + } + + } + + businessEventNotifierService.notifyPostBusinessEvent(new SavingsCloseBusinessEvent(account)); + // disable all standing orders linked to the savings account + disableStandingInstructionsLinkedToClosedSavings(account); + return new CommandProcessingResultBuilder() // + .withEntityId(savingsId) // + .withOfficeId(account.officeId()) // + .withClientId(account.clientId()) // + .withGroupId(account.groupId()) // + .withSavingsId(savingsId) // + .with(changes) // + .build(); + } + + @Override + public SavingsAccountTransaction initiateSavingsTransfer(final SavingsAccount savingsAccount, final LocalDate transferDate) { + getAppUserIfPresent(); + + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + + validateTransactionsForTransfer(savingsAccount, transferDate); + + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + this.savingAccountAssembler.setHelpers(savingsAccount); + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + updateExistingTransactionsDetails(savingsAccount, existingTransactionIds, existingReversedTransactionIds); + + final SavingsAccountTransaction newTransferTransaction = SavingsAccountTransaction.initiateTransfer(savingsAccount, + savingsAccount.office(), transferDate); + savingsAccount.addTransaction(newTransferTransaction); + savingsAccount.setStatus(SavingsAccountStatusType.TRANSFER_IN_PROGRESS.getValue()); + final MathContext mc = MathContext.DECIMAL64; + boolean isInterestTransfer = false; + LocalDate postInterestOnDate = null; + boolean postReversals = false; + savingsAccount.calculateInterestUsing(mc, transferDate, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth, postInterestOnDate, false, postReversals); + + this.savingsAccountTransactionRepository.save(newTransferTransaction); + this.savingAccountRepositoryWrapper.saveAndFlush(savingsAccount); + + postJournalEntries(savingsAccount, existingTransactionIds, existingReversedTransactionIds, false); + + return newTransferTransaction; + } + + @Override + public SavingsAccountTransaction withdrawSavingsTransfer(final SavingsAccount savingsAccount, final LocalDate transferDate) { + getAppUserIfPresent(); + + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + this.savingAccountAssembler.setHelpers(savingsAccount); + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + updateExistingTransactionsDetails(savingsAccount, existingTransactionIds, existingReversedTransactionIds); + + final SavingsAccountTransaction withdrawtransferTransaction = SavingsAccountTransaction.withdrawTransfer(savingsAccount, + savingsAccount.office(), transferDate); + savingsAccount.addTransaction(withdrawtransferTransaction); + savingsAccount.setStatus(SavingsAccountStatusType.ACTIVE.getValue()); + final MathContext mc = MathContext.DECIMAL64; + boolean isInterestTransfer = false; + LocalDate postInterestOnDate = null; + boolean postReversals = false; + savingsAccount.calculateInterestUsing(mc, transferDate, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth, postInterestOnDate, false, postReversals); + + this.savingsAccountTransactionRepository.save(withdrawtransferTransaction); + this.savingAccountRepositoryWrapper.saveAndFlush(savingsAccount); + + postJournalEntries(savingsAccount, existingTransactionIds, existingReversedTransactionIds, false); + + return withdrawtransferTransaction; + } + + @Override + public void rejectSavingsTransfer(final SavingsAccount savingsAccount) { + this.savingAccountAssembler.setHelpers(savingsAccount); + savingsAccount.setStatus(SavingsAccountStatusType.TRANSFER_ON_HOLD.getValue()); + this.savingAccountRepositoryWrapper.save(savingsAccount); + } + + @Override + public SavingsAccountTransaction acceptSavingsTransfer(final SavingsAccount savingsAccount, final LocalDate transferDate, + final Office acceptedInOffice, final Staff fieldOfficer) { + getAppUserIfPresent(); + + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + this.savingAccountAssembler.setHelpers(savingsAccount); + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + updateExistingTransactionsDetails(savingsAccount, existingTransactionIds, existingReversedTransactionIds); + + final SavingsAccountTransaction acceptTransferTransaction = SavingsAccountTransaction.approveTransfer(savingsAccount, + acceptedInOffice, transferDate); + savingsAccount.addTransaction(acceptTransferTransaction); + savingsAccount.setStatus(SavingsAccountStatusType.ACTIVE.getValue()); + if (fieldOfficer != null) { + savingsAccount.reassignSavingsOfficer(fieldOfficer, transferDate); + } + boolean isInterestTransfer = false; + final MathContext mc = MathContext.DECIMAL64; + LocalDate postInterestOnDate = null; + boolean postReversals = false; + savingsAccount.calculateInterestUsing(mc, transferDate, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth, postInterestOnDate, false, postReversals); + + this.savingsAccountTransactionRepository.save(acceptTransferTransaction); + this.savingAccountRepositoryWrapper.saveAndFlush(savingsAccount); + + postJournalEntries(savingsAccount, existingTransactionIds, existingReversedTransactionIds, false); + + return acceptTransferTransaction; + } + + @Transactional + @Override + public CommandProcessingResult addSavingsAccountCharge(final JsonCommand command) { + this.context.authenticatedUser(); + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(SAVINGS_ACCOUNT_RESOURCE_NAME); + + final Long savingsAccountId = command.getSavingsId(); + this.savingsAccountChargeDataValidator.validateAdd(command.json()); + + final SavingsAccount savingsAccount = this.savingAccountAssembler.assembleFrom(savingsAccountId, false); + checkClientOrGroupActive(savingsAccount); + + final Locale locale = command.extractLocale(); + final String format = command.dateFormat(); + final DateTimeFormatter fmt = StringUtils.isNotBlank(format) ? DateTimeFormatter.ofPattern(format).withLocale(locale) + : DateTimeFormatter.ofPattern("dd MM yyyy"); + + final Long chargeDefinitionId = command.longValueOfParameterNamed(chargeIdParamName); + final Charge chargeDefinition = this.chargeRepository.findOneWithNotFoundDetection(chargeDefinitionId); + + Integer chargeTimeType = chargeDefinition.getChargeTimeType(); + LocalDate dueAsOfDateParam = command.localDateValueOfParameterNamed(dueAsOfDateParamName); + if ((chargeTimeType.equals(ChargeTimeType.WITHDRAWAL_FEE.getValue()) + || chargeTimeType.equals(ChargeTimeType.SAVINGS_NOACTIVITY_FEE.getValue())) && dueAsOfDateParam != null) { + baseDataValidator.reset().parameter(dueAsOfDateParamName).value(dueAsOfDateParam.format(fmt)) + .failWithCodeNoParameterAddedToErrorCode( + "charge.due.date.is.invalid.for." + ChargeTimeType.fromInt(chargeTimeType).getCode()); + } + final SavingsAccountCharge savingsAccountCharge = SavingsAccountCharge.createNewFromJson(savingsAccount, chargeDefinition, command); + + if (chargeDefinition.isEnableFreeWithdrawal()) { + savingsAccountCharge.setFreeWithdrawalCount(0); + } + + if (savingsAccountCharge.getDueDate() != null) { + // transaction date should not be on a holiday or non working day + if (!this.configurationDomainService.allowTransactionsOnHolidayEnabled() + && this.holidayRepository.isHoliday(savingsAccount.officeId(), savingsAccountCharge.getDueDate())) { + baseDataValidator.reset().parameter(dueAsOfDateParamName).value(savingsAccountCharge.getDueDate().format(fmt)) + .failWithCodeNoParameterAddedToErrorCode("charge.due.date.is.on.holiday"); + } + + if (!this.configurationDomainService.allowTransactionsOnNonWorkingDayEnabled() + && !this.workingDaysRepository.isWorkingDay(savingsAccountCharge.getDueDate())) { + baseDataValidator.reset().parameter(dueAsOfDateParamName).value(savingsAccountCharge.getDueDate().format(fmt)) + .failWithCodeNoParameterAddedToErrorCode("charge.due.date.is.a.nonworking.day"); + } + } + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + + savingsAccount.addCharge(fmt, savingsAccountCharge, chargeDefinition); + this.savingsAccountChargeRepository.save(savingsAccountCharge); + this.savingAccountRepositoryWrapper.saveAndFlush(savingsAccount); + return new CommandProcessingResultBuilder() // + .withEntityId(savingsAccountCharge.getId()) // + .withOfficeId(savingsAccount.officeId()) // + .withClientId(savingsAccount.clientId()) // + .withGroupId(savingsAccount.groupId()) // + .withSavingsId(savingsAccountId) // + .build(); + } + + @Transactional + @Override + public CommandProcessingResult updateSavingsAccountCharge(final JsonCommand command) { + + this.context.authenticatedUser(); + this.savingsAccountChargeDataValidator.validateUpdate(command.json()); + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(SAVINGS_ACCOUNT_RESOURCE_NAME); + + final Long savingsAccountId = command.getSavingsId(); + // SavingsAccount Charge entity + final Long savingsChargeId = command.entityId(); + + final SavingsAccount savingsAccount = this.savingAccountAssembler.assembleFrom(savingsAccountId, false); + checkClientOrGroupActive(savingsAccount); + + final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository.findOneWithNotFoundDetection(savingsChargeId, + savingsAccountId); + + final Map changes = savingsAccountCharge.update(command); + + if (savingsAccountCharge.getDueDate() != null) { + final Locale locale = command.extractLocale(); + final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); + + // transaction date should not be on a holiday or non working day + if (!this.configurationDomainService.allowTransactionsOnHolidayEnabled() + && this.holidayRepository.isHoliday(savingsAccount.officeId(), savingsAccountCharge.getDueDate())) { + baseDataValidator.reset().parameter(dueAsOfDateParamName).value(savingsAccountCharge.getDueDate().format(fmt)) + .failWithCodeNoParameterAddedToErrorCode("charge.due.date.is.on.holiday"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + if (!this.configurationDomainService.allowTransactionsOnNonWorkingDayEnabled() + && !this.workingDaysRepository.isWorkingDay(savingsAccountCharge.getDueDate())) { + baseDataValidator.reset().parameter(dueAsOfDateParamName).value(savingsAccountCharge.getDueDate().format(fmt)) + .failWithCodeNoParameterAddedToErrorCode("charge.due.date.is.a.nonworking.day"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + } + + this.savingsAccountChargeRepository.saveAndFlush(savingsAccountCharge); + + return new CommandProcessingResultBuilder() // + .withEntityId(savingsAccountCharge.getId()) // + .withOfficeId(savingsAccountCharge.savingsAccount().officeId()) // + .withClientId(savingsAccountCharge.savingsAccount().clientId()) // + .withGroupId(savingsAccountCharge.savingsAccount().groupId()) // + .withSavingsId(savingsAccountCharge.savingsAccount().getId()) // + .with(changes) // + .build(); + } + + @Transactional + @Override + public CommandProcessingResult waiveCharge(final Long savingsAccountId, final Long savingsAccountChargeId) { + context.authenticatedUser(); + + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + + final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository + .findOneWithNotFoundDetection(savingsAccountChargeId, savingsAccountId); + + // Get Savings account from savings charge + final SavingsAccount account = savingsAccountCharge.savingsAccount(); + + final boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus(); + + this.savingAccountAssembler.loadTransactionsToSavingsAccount(account, backdatedTxnsAllowedTill); + + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + + if (backdatedTxnsAllowedTill) { + updateSavingsTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + } else { + updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + } + + account.waiveCharge(savingsAccountChargeId, backdatedTxnsAllowedTill); + + boolean isInterestTransfer = false; + LocalDate postInterestOnDate = null; + final MathContext mc = MathContext.DECIMAL64; + boolean postReversals = false; + if (account.isBeforeLastPostingPeriod(savingsAccountCharge.getDueDate(), backdatedTxnsAllowedTill)) { + final LocalDate today = DateUtils.getBusinessLocalDate(); + account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, + postInterestOnDate, backdatedTxnsAllowedTill, postReversals); + } else { + final LocalDate today = DateUtils.getBusinessLocalDate(); + account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals); + } + + List depositAccountOnHoldTransactions = null; + if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { + depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository + .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); + } + + account.validateAccountBalanceDoesNotBecomeNegative(SavingsApiConstants.waiveChargeTransactionAction, + depositAccountOnHoldTransactions, backdatedTxnsAllowedTill); + + if (backdatedTxnsAllowedTill) { + this.savingsAccountTransactionRepository.saveAll(account.getSavingsAccountTransactionsWithPivotConfig()); + } + + this.savingAccountRepositoryWrapper.saveAndFlush(account); + + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, backdatedTxnsAllowedTill); + return new CommandProcessingResultBuilder() // + .withEntityId(savingsAccountChargeId) // + .withOfficeId(account.officeId()) // + .withClientId(account.clientId()) // + .withGroupId(account.groupId()) // + .withSavingsId(savingsAccountId) // + .build(); + } + + @Transactional + @Override + public CommandProcessingResult deleteSavingsAccountCharge(final Long savingsAccountId, final Long savingsAccountChargeId, + @SuppressWarnings("unused") final JsonCommand command) { + this.context.authenticatedUser(); + + final SavingsAccount savingsAccount = this.savingAccountAssembler.assembleFrom(savingsAccountId, false); + checkClientOrGroupActive(savingsAccount); + final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository + .findOneWithNotFoundDetection(savingsAccountChargeId, savingsAccountId); + + savingsAccount.removeCharge(savingsAccountCharge); + this.savingAccountRepositoryWrapper.saveAndFlush(savingsAccount); + + return new CommandProcessingResultBuilder() // + .withEntityId(savingsAccountChargeId) // + .withOfficeId(savingsAccount.officeId()) // + .withClientId(savingsAccount.clientId()) // + .withGroupId(savingsAccount.groupId()) // + .withSavingsId(savingsAccountId) // + .build(); + } + + @Override + public CommandProcessingResult payCharge(final Long savingsAccountId, final Long savingsAccountChargeId, final JsonCommand command) { + context.authenticatedUser(); + + this.savingsAccountChargeDataValidator.validatePayCharge(command.json()); + final Locale locale = command.extractLocale(); + final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); + final BigDecimal amountPaid = command.bigDecimalValueOfParameterNamed(amountParamName); + final LocalDate transactionDate = command.localDateValueOfParameterNamed(dueAsOfDateParamName); + + final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository + .findOneWithNotFoundDetection(savingsAccountChargeId, savingsAccountId); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(SAVINGS_ACCOUNT_RESOURCE_NAME); + + // transaction date should not be on a holiday or non working day + if (!this.configurationDomainService.allowTransactionsOnHolidayEnabled() + && this.holidayRepository.isHoliday(savingsAccountCharge.savingsAccount().officeId(), transactionDate)) { + baseDataValidator.reset().parameter(dueAsOfDateParamName).value(transactionDate.format(fmt)) + .failWithCodeNoParameterAddedToErrorCode("transaction.not.allowed.transaction.date.is.on.holiday"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + if (!this.configurationDomainService.allowTransactionsOnNonWorkingDayEnabled() + && !this.workingDaysRepository.isWorkingDay(transactionDate)) { + baseDataValidator.reset().parameter(dueAsOfDateParamName).value(transactionDate.format(fmt)) + .failWithCodeNoParameterAddedToErrorCode("transaction.not.allowed.transaction.date.is.a.nonworking.day"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + final boolean backdatedTxnsAllowedTill = false; + + SavingsAccountTransaction chargeTransaction = this.payCharge(savingsAccountCharge, transactionDate, amountPaid, fmt, + backdatedTxnsAllowedTill); + + final String noteText = command.stringValueOfParameterNamed("note"); + if (StringUtils.isNotBlank(noteText)) { + final Note note = Note.savingsTransactionNote(savingsAccountCharge.savingsAccount(), chargeTransaction, noteText); + this.noteRepository.save(note); + } + + return new CommandProcessingResultBuilder() // + .withEntityId(savingsAccountCharge.getId()) // + .withOfficeId(savingsAccountCharge.savingsAccount().officeId()) // + .withClientId(savingsAccountCharge.savingsAccount().clientId()) // + .withGroupId(savingsAccountCharge.savingsAccount().groupId()) // + .withSavingsId(savingsAccountCharge.savingsAccount().getId()) // + .build(); + + } + + @Transactional + @Override + public void applyChargeDue(final Long savingsAccountChargeId, final Long accountId) { + // always use current date as transaction date for batch job + final LocalDate transactionDate = DateUtils.getBusinessLocalDate(); + final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository + .findOneWithNotFoundDetection(savingsAccountChargeId, accountId); + + final DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd MM yyyy").withZone(DateUtils.getDateTimeZoneOfTenant()); + + while (savingsAccountCharge.isNotFullyPaid() && DateUtils.isBefore(savingsAccountCharge.getDueDate(), transactionDate)) { + payCharge(savingsAccountCharge, transactionDate, savingsAccountCharge.amoutOutstanding(), fmt, false); + } + } + + @SuppressWarnings("unused") + public SavingsAccountData fallbackPostInterest(SavingsAccountData savingsAccountData, boolean postInterestAs, LocalDate transactionDate, + boolean backdatedTxnsAllowedTill, Throwable t) { + // NOTE: allow caller to catch the exceptions + throw org.apache.fineract.infrastructure.core.exception.ErrorHandler.getMappable(t, null, null, "savings.postinterest"); + } + + @Transactional + private SavingsAccountTransaction payCharge(final SavingsAccountCharge savingsAccountCharge, final LocalDate transactionDate, + final BigDecimal amountPaid, final DateTimeFormatter formatter, final boolean backdatedTxnsAllowedTill) { + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + + // Get Savings account from savings charge + final SavingsAccount account = savingsAccountCharge.savingsAccount(); + this.savingAccountAssembler.assignSavingAccountHelpers(account); + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + Pageable sortedByDateAndIdDesc = PageRequest.of(0, 1, Sort.by("dateOf", "id").descending()); + + List savingsAccountTransaction = this.savingsAccountTransactionRepository + .findBySavingsAccountIdAndLessThanDateOfAndReversedIsFalse(account.getId(), transactionDate, sortedByDateAndIdDesc); + + account.validateAccountBalanceDoesNotViolateOverdraft(savingsAccountTransaction, amountPaid); + + updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + SavingsAccountTransaction chargeTransaction = account.payCharge(savingsAccountCharge, amountPaid, transactionDate, formatter, + backdatedTxnsAllowedTill, null); + boolean isInterestTransfer = false; + LocalDate postInterestOnDate = null; + final MathContext mc = MathContext.DECIMAL64; + boolean postReversals = false; + if (account.isBeforeLastPostingPeriod(transactionDate, backdatedTxnsAllowedTill)) { + final LocalDate today = DateUtils.getBusinessLocalDate(); + account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, + postInterestOnDate, isInterestTransfer, postReversals); + } else { + final LocalDate today = DateUtils.getBusinessLocalDate(); + account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals); + } + List depositAccountOnHoldTransactions = null; + + if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { + depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository + .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); + } + + account.validateAccountBalanceDoesNotBecomeNegative("." + SavingsAccountTransactionType.PAY_CHARGE.getCode(), + depositAccountOnHoldTransactions, backdatedTxnsAllowedTill); + + saveTransactionToGenerateTransactionId(chargeTransaction); + + this.savingAccountRepositoryWrapper.saveAndFlush(account); + + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, backdatedTxnsAllowedTill); + + return chargeTransaction; + } + + private void updateExistingTransactionsDetails(SavingsAccount account, Set existingTransactionIds, + Set existingReversedTransactionIds) { + existingTransactionIds.addAll(account.findExistingTransactionIds()); + existingReversedTransactionIds.addAll(account.findExistingReversedTransactionIds()); + } + + private void updateExistingTransactionsDetails(SavingsAccountData account, Set existingTransactionIds, + Set existingReversedTransactionIds) { + existingTransactionIds.addAll(account.findCurrentTransactionIdsWithPivotDateConfig()); + existingReversedTransactionIds.addAll(account.findCurrentReversedTransactionIdsWithPivotDateConfig()); + } + + private void updateSavingsTransactionsDetails(SavingsAccount account, Set existingTransactionIds, + Set existingReversedTransactionIds) { + existingTransactionIds.addAll(account.findCurrentTransactionIdsWithPivotDateConfig()); + existingReversedTransactionIds.addAll(account.findCurrentReversedTransactionIdsWithPivotDateConfig()); + } + + private void postJournalEntries(final SavingsAccount savingsAccount, final Set existingTransactionIds, + final Set existingReversedTransactionIds, final boolean backdatedTxnsAllowedTill) { + + boolean isAccountTransfer = false; + final Map accountingBridgeData = savingsAccount.deriveAccountingBridgeData(savingsAccount.getCurrency().getCode(), + existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill); + this.journalEntryWritePlatformService.createJournalEntriesForSavings(accountingBridgeData); + } + + @Override + public CommandProcessingResult inactivateCharge(final Long savingsAccountId, final Long savingsAccountChargeId) { + + this.context.authenticatedUser(); + + final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository + .findOneWithNotFoundDetection(savingsAccountChargeId, savingsAccountId); + + final SavingsAccount account = savingsAccountCharge.savingsAccount(); + this.savingAccountAssembler.assignSavingAccountHelpers(account); + + final LocalDate inactivationOnDate = DateUtils.getBusinessLocalDate(); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(SAVINGS_ACCOUNT_CHARGE_RESOURCE_NAME); + + // Only recurring fees are allowed to inactivate + if (!savingsAccountCharge.isRecurringFee()) { + baseDataValidator.reset().parameter(null).value(savingsAccountCharge.getId()) + .failWithCodeNoParameterAddedToErrorCode("charge.inactivation.allowed.only.for.recurring.charges"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + + } else { + final LocalDate nextDueDate = savingsAccountCharge.getNextDueDateFrom(inactivationOnDate); + + if (savingsAccountCharge.isChargeIsDue(nextDueDate)) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("inactivation.of.charge.not.allowed.when.charge.is.due"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } else if (savingsAccountCharge.isChargeIsOverPaid(nextDueDate)) { + + final List chargePayments = new ArrayList<>(); + SavingsAccountCharge updatedCharge = savingsAccountCharge; + do { + chargePayments.clear(); + for (SavingsAccountTransaction transaction : account.getTransactions()) { + if (transaction.isPayCharge() && transaction.isNotReversed() + && transaction.isPaymentForCurrentCharge(savingsAccountCharge)) { + chargePayments.add(transaction); + } + } + // Reverse the excess payments of charge transactions + SavingsAccountTransaction lastChargePayment = getLastChargePayment(chargePayments); + this.undoTransaction(savingsAccountCharge.savingsAccount().getId(), lastChargePayment.getId(), false); + updatedCharge = account.getUpdatedChargeDetails(savingsAccountCharge); + } while (updatedCharge.isChargeIsOverPaid(nextDueDate)); + } + account.inactivateCharge(savingsAccountCharge, inactivationOnDate); + } + + return new CommandProcessingResultBuilder() // + .withEntityId(savingsAccountCharge.getId()) // + .withOfficeId(savingsAccountCharge.savingsAccount().officeId()) // + .withClientId(savingsAccountCharge.savingsAccount().clientId()) // + .withGroupId(savingsAccountCharge.savingsAccount().groupId()) // + .withSavingsId(savingsAccountCharge.savingsAccount().getId()) // + .build(); + } + + private SavingsAccountTransaction getLastChargePayment(final List chargePayments) { + if (!CollectionUtils.isEmpty(chargePayments)) { + return chargePayments.get(chargePayments.size() - 1); + } + return null; + } + + @Transactional + @Override + public CommandProcessingResult assignFieldOfficer(Long savingsAccountId, JsonCommand command) { + this.context.authenticatedUser(); + final Map actualChanges = new LinkedHashMap<>(5); + + Staff fromSavingsOfficer = null; + Staff toSavingsOfficer = null; + this.fromApiJsonDeserializer.validateForAssignSavingsOfficer(command.json()); + + final SavingsAccount savingsForUpdate = this.savingAccountRepositoryWrapper.findOneWithNotFoundDetection(savingsAccountId); + final Long fromSavingsOfficerId = command.longValueOfParameterNamed("fromSavingsOfficerId"); + final Long toSavingsOfficerId = command.longValueOfParameterNamed("toSavingsOfficerId"); + final LocalDate dateOfSavingsOfficerAssignment = command.localDateValueOfParameterNamed("assignmentDate"); + + if (fromSavingsOfficerId != null) { + fromSavingsOfficer = this.staffRepository.findByOfficeHierarchyWithNotFoundDetection(fromSavingsOfficerId, + savingsForUpdate.office().getHierarchy()); + } + if (toSavingsOfficerId != null) { + toSavingsOfficer = this.staffRepository.findByOfficeHierarchyWithNotFoundDetection(toSavingsOfficerId, + savingsForUpdate.office().getHierarchy()); + actualChanges.put("toSavingsOfficerId", toSavingsOfficer.getId()); + } + if (!savingsForUpdate.hasSavingsOfficer(fromSavingsOfficer)) { + throw new SavingsOfficerAssignmentException(savingsAccountId, fromSavingsOfficerId); + } + + savingsForUpdate.reassignSavingsOfficer(toSavingsOfficer, dateOfSavingsOfficerAssignment); + + this.savingAccountRepositoryWrapper.saveAndFlush(savingsForUpdate); + + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .withOfficeId(savingsForUpdate.officeId()) // + .withEntityId(savingsForUpdate.getId()) // + .withSavingsId(savingsAccountId) // + .with(actualChanges) // + .build(); + } + + @Transactional + @Override + public CommandProcessingResult unassignFieldOfficer(Long savingsAccountId, JsonCommand command) { + + this.context.authenticatedUser(); + + final Map actualChanges = new LinkedHashMap<>(5); + this.fromApiJsonDeserializer.validateForUnAssignSavingsOfficer(command.json()); + + final SavingsAccount savingsForUpdate = this.savingAccountRepositoryWrapper.findOneWithNotFoundDetection(savingsAccountId); + if (savingsForUpdate.getSavingsOfficer() == null) { + throw new SavingsOfficerUnassignmentException(savingsAccountId); + } + + final LocalDate dateOfSavingsOfficerUnassigned = command.localDateValueOfParameterNamed("unassignedDate"); + + savingsForUpdate.removeSavingsOfficer(dateOfSavingsOfficerUnassigned); + + this.savingAccountRepositoryWrapper.saveAndFlush(savingsForUpdate); + + actualChanges.put("toSavingsOfficerId", null); + + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .withOfficeId(savingsForUpdate.officeId()) // + .withEntityId(savingsForUpdate.getId()) // + .withSavingsId(savingsAccountId) // + .with(actualChanges) // + .build(); + } + + @Override + public CommandProcessingResult modifyWithHoldTax(Long savingsAccountId, JsonCommand command) { + final Map actualChanges = new HashMap<>(1); + final SavingsAccount savingsForUpdate = this.savingAccountRepositoryWrapper.findOneWithNotFoundDetection(savingsAccountId); + if (command.isChangeInBooleanParameterNamed(withHoldTaxParamName, savingsForUpdate.withHoldTax())) { + final boolean newValue = command.booleanPrimitiveValueOfParameterNamed(withHoldTaxParamName); + actualChanges.put(withHoldTaxParamName, newValue); + savingsForUpdate.setWithHoldTax(newValue); + if (savingsForUpdate.getTaxGroup() == null) { + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("account"); + baseDataValidator.reset().parameter(withHoldTaxParamName).failWithCode("not.supported"); + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .withEntityId(savingsAccountId) // + .withSavingsId(savingsAccountId) // + .with(actualChanges) // + .build(); + } + + @Override + public void setSubStatusInactive(Long savingsId) { + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + account.setSubStatusInactive(false); + this.savingAccountRepositoryWrapper.saveAndFlush(account); + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false); + } + + @Override + public void setSubStatusDormant(Long savingsId) { + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); + account.setSubStatusDormant(); + this.savingAccountRepositoryWrapper.saveAndFlush(account); + } + + @Override + public void escheat(Long savingsId) { + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + account.escheat(appuserRepository.fetchSystemUser()); + this.savingAccountRepositoryWrapper.saveAndFlush(account); + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false); + } + + private AppUser getAppUserIfPresent() { + AppUser user = null; + if (this.context != null) { + user = this.context.getAuthenticatedUserIfPresent(); + } + return user; + } + + /** + * Disable all standing instructions linked to the savings account if the status is "closed" + * + * @param savingsAccount + * -- the savings account object + * + **/ + @Transactional + private void disableStandingInstructionsLinkedToClosedSavings(final SavingsAccount savingsAccount) { + if (savingsAccount != null && savingsAccount.isClosed()) { + final Integer standingInstructionStatus = StandingInstructionStatus.ACTIVE.getValue(); + final Collection accountTransferStandingInstructions = this.standingInstructionRepository + .findBySavingsAccountAndStatus(savingsAccount, standingInstructionStatus); + + if (!accountTransferStandingInstructions.isEmpty()) { + for (AccountTransferStandingInstruction accountTransferStandingInstruction : accountTransferStandingInstructions) { + accountTransferStandingInstruction.updateStatus(StandingInstructionStatus.DISABLED.getValue()); + this.standingInstructionRepository.save(accountTransferStandingInstruction); + } + } + } + } + + @Override + public CommandProcessingResult blockAccount(final Long savingsId, final JsonCommand command) { + + this.context.authenticatedUser(); + + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); + checkClientOrGroupActive(account); + + final Map changes = account.block(); + + final String reasonForBlock = command.stringValueOfParameterNamed(SavingsApiConstants.reasonForBlockParamName); + validateReasonForHold(reasonForBlock); + account.updateReason(reasonForBlock); + + if (!changes.isEmpty()) { + + this.savingAccountRepositoryWrapper.save(account); + } + return new CommandProcessingResultBuilder().withEntityId(savingsId).withOfficeId(account.officeId()) + .withClientId(account.clientId()).withGroupId(account.groupId()).withSavingsId(savingsId).with(changes).build(); + } + + @Override + public CommandProcessingResult unblockAccount(final Long savingsId) { + this.context.authenticatedUser(); + + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); + checkClientOrGroupActive(account); + + final Map changes = account.unblock(); + + account.updateReason(null); + + if (!changes.isEmpty()) { + + this.savingAccountRepositoryWrapper.save(account); + } + return new CommandProcessingResultBuilder().withEntityId(savingsId).withOfficeId(account.officeId()) + .withClientId(account.clientId()).withGroupId(account.groupId()).withSavingsId(savingsId).with(changes).build(); + } + + @Transactional + @Override + public CommandProcessingResult holdAmount(final Long savingsId, final JsonCommand command) { + + final AppUser submittedBy = this.context.authenticatedUser(); + final boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus(); + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, backdatedTxnsAllowedTill); + final LocalDate transactionDate = command.localDateValueOfParameterNamed(transactionDateParamName); + final boolean lienAllowed = command.booleanPrimitiveValueOfParameterNamed(lienAllowedParamName); + + checkClientOrGroupActive(account); + + final BigDecimal amount = command.bigDecimalValueOfParameterNamed(transactionAmountParamName); + + Money runningBalance = Money.of(account.getCurrency(), account.getAccountBalance()); + if (account.getSavingsHoldAmount() != null) { + runningBalance = runningBalance.minus(account.getSavingsHoldAmount()).minus(amount); + } else { + runningBalance = runningBalance.minus(amount); + } + + this.savingsAccountTransactionDataValidator.validateHoldAndAssembleForm(command.json(), account, submittedBy, + backdatedTxnsAllowedTill); + SavingsAccountTransaction transaction = this.savingsAccountDomainService.handleHold(account, amount, transactionDate, lienAllowed); + account.holdAmount(amount); + transaction.setRunningBalance(runningBalance); + + final String reasonForBlock = command.stringValueOfParameterNamed(SavingsApiConstants.reasonForBlockParamName); + transaction.updateReason(reasonForBlock); + + account.getAccountBalance(); + this.savingsAccountTransactionDataValidator.validateTransactionWithPivotDate(transaction.getTransactionDate(), account); + + this.savingsAccountTransactionRepository.saveAndFlush(transaction); + + if (backdatedTxnsAllowedTill) { + // Check again whether transactions are modified + this.savingsAccountTransactionRepository.saveAll(account.getSavingsAccountTransactionsWithPivotConfig()); + } + + this.savingAccountRepositoryWrapper.saveAndFlush(account); + + return new CommandProcessingResultBuilder().withEntityId(transaction.getId()).withOfficeId(account.officeId()) + .withClientId(account.clientId()).withGroupId(account.groupId()).withSavingsId(savingsId).build(); + } + + @Transactional + @Override + public CommandProcessingResult releaseAmount(final Long savingsId, final Long savingsTransactionId) { + context.authenticatedUser(); + SavingsAccountTransaction holdTransaction = this.savingsAccountTransactionRepository + .findOneByIdAndSavingsAccountId(savingsTransactionId, savingsId); + + holdTransaction.updateReason(null); + + final SavingsAccountTransaction transaction = this.savingsAccountTransactionDataValidator + .validateReleaseAmountAndAssembleForm(holdTransaction); + + final boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus(); + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, backdatedTxnsAllowedTill); + checkClientOrGroupActive(account); + + Money runningBalance = Money.of(account.getCurrency(), account.getAccountBalance()); + + Money savingsOnHold = Money.of(account.getCurrency(), account.getSavingsHoldAmount()); + + runningBalance = runningBalance.minus(savingsOnHold); + + runningBalance = runningBalance.plus(transaction.getAmount()); + transaction.setRunningBalance(runningBalance); + + this.savingsAccountTransactionDataValidator.validateTransactionWithPivotDate(transaction.getTransactionDate(), account); + account.releaseOnHoldAmount(transaction.getAmount()); + + this.savingsAccountTransactionRepository.saveAndFlush(transaction); + holdTransaction.updateReleaseId(transaction.getId()); + + if (backdatedTxnsAllowedTill) { + this.savingsAccountTransactionRepository.saveAll(account.getSavingsAccountTransactionsWithPivotConfig()); + } else { + account.addTransaction(transaction); + } + + this.savingAccountRepositoryWrapper.save(account); + + return new CommandProcessingResultBuilder().withEntityId(transaction.getId()).withOfficeId(account.officeId()) + .withClientId(account.clientId()).withGroupId(account.groupId()).withSavingsId(account.getId()).build(); + } + + @Transactional + @Override + public CommandProcessingResult blockCredits(final Long savingsId, final JsonCommand command) { + this.context.authenticatedUser(); + + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); + checkClientOrGroupActive(account); + + final String reasonForBlock = command.stringValueOfParameterNamed(SavingsApiConstants.reasonForBlockParamName); + validateReasonForHold(reasonForBlock); + account.updateReason(reasonForBlock); + + final Map changes = account.blockCredits(account.getSubStatus()); + if (!changes.isEmpty()) { + + this.savingAccountRepositoryWrapper.save(account); + } + return new CommandProcessingResultBuilder().withEntityId(savingsId).withOfficeId(account.officeId()) + .withClientId(account.clientId()).withGroupId(account.groupId()).withSavingsId(savingsId).with(changes).build(); + } + + @Transactional + @Override + public CommandProcessingResult unblockCredits(final Long savingsId) { + this.context.authenticatedUser(); + + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); + checkClientOrGroupActive(account); + account.updateReason(null); + final Map changes = account.unblockCredits(); + if (!changes.isEmpty()) { + + this.savingAccountRepositoryWrapper.save(account); + } + return new CommandProcessingResultBuilder().withEntityId(savingsId).withOfficeId(account.officeId()) + .withClientId(account.clientId()).withGroupId(account.groupId()).withSavingsId(savingsId).with(changes).build(); + } + + @Transactional + @Override + public CommandProcessingResult blockDebits(final Long savingsId, final JsonCommand command) { + this.context.authenticatedUser(); + + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); + checkClientOrGroupActive(account); + + final String reasonForBlock = command.stringValueOfParameterNamed(SavingsApiConstants.reasonForBlockParamName); + validateReasonForHold(reasonForBlock); + account.updateReason(reasonForBlock); + + final Map changes = account.blockDebits(account.getSubStatus()); + if (!changes.isEmpty()) { + + this.savingAccountRepositoryWrapper.save(account); + } + return new CommandProcessingResultBuilder().withEntityId(savingsId).withOfficeId(account.officeId()) + .withClientId(account.clientId()).withGroupId(account.groupId()).withSavingsId(savingsId).with(changes).build(); + } + + @Transactional + @Override + public CommandProcessingResult unblockDebits(final Long savingsId) { + this.context.authenticatedUser(); + + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); + checkClientOrGroupActive(account); + + account.updateReason(null); + + final Map changes = account.unblockDebits(); + if (!changes.isEmpty()) { + + this.savingAccountRepositoryWrapper.save(account); + } + return new CommandProcessingResultBuilder().withEntityId(savingsId).withOfficeId(account.officeId()) + .withClientId(account.clientId()).withGroupId(account.groupId()).withSavingsId(savingsId).with(changes).build(); + } + + private void validateTransactionsForTransfer(final SavingsAccount savingsAccount, final LocalDate transferDate) { + for (SavingsAccountTransaction transaction : savingsAccount.getTransactions()) { + if ((DateUtils.isEqual(transferDate, transaction.getTransactionDate()) + && DateUtils.isEqual(transferDate, transaction.getSubmittedOnDate())) + || DateUtils.isBefore(transferDate, transaction.getTransactionDate())) { + throw new GeneralPlatformDomainRuleException(TransferApiConstants.transferClientSavingsException, + TransferApiConstants.transferClientSavingsException, transaction.getTransactionDate(), transferDate); + } + } + } + + private void validateReasonForHold(String reasonForBlock) { + if (StringUtils.isBlank(reasonForBlock)) { + throw new PlatformDataIntegrityException("Reason For Block is Mandatory", "error.msg.reason.for.block.mandatory"); + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java index ef4d08f58..2079c29e5 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java @@ -98,6 +98,7 @@ import org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityConfig import org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityLimit; import org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityLimitRepository; import org.apache.fineract.portfolio.savings.domain.SavingsProductRepository; +import org.apache.fineract.portfolio.denominationmaster.domain.SavingsTxnDenominationRepository; import org.apache.fineract.portfolio.savings.service.FacilityChargeAutoApplyService; import org.apache.fineract.portfolio.savings.service.DepositAccountInterestRateChartReadPlatformService; import org.apache.fineract.portfolio.savings.service.DepositAccountInterestRateChartReadPlatformServiceImpl; @@ -424,7 +425,8 @@ public class SavingsConfiguration { EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService, AppUserRepositoryWrapper appuserRepository, StandingInstructionRepository standingInstructionRepository, BusinessEventNotifierService businessEventNotifierService, GSIMRepositoy gsimRepository, SavingsAccountInterestPostingService savingsAccountInterestPostingService, - ErrorHandler errorHandler, FacilityChargeAutoApplyService facilityChargeAutoApplyService) { + ErrorHandler errorHandler, FacilityChargeAutoApplyService facilityChargeAutoApplyService, + SavingsTxnDenominationRepository savingsTxnDenominationRepository) { return new SavingsAccountWritePlatformServiceJpaRepositoryImpl(context, fromApiJsonDeserializer, savingAccountRepositoryWrapper, staffRepository, savingsAccountTransactionRepository, savingAccountAssembler, savingsAccountTransactionDataValidator, savingsAccountChargeDataValidator, paymentDetailWritePlatformService, journalEntryWritePlatformService, @@ -432,7 +434,7 @@ public class SavingsConfiguration { chargeRepository, savingsAccountChargeRepository, holidayRepository, workingDaysRepository, configurationDomainService, depositAccountOnHoldTransactionRepository, entityDatatableChecksWritePlatformService, appuserRepository, standingInstructionRepository, businessEventNotifierService, gsimRepository, savingsAccountInterestPostingService, - errorHandler, facilityChargeAutoApplyService); + errorHandler, facilityChargeAutoApplyService, savingsTxnDenominationRepository); } @Bean diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index e640e4ace..ad7639229 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -318,4 +318,5 @@ + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0012_denomination_master.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0012_denomination_master.xml new file mode 100644 index 000000000..aad95f9a8 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0012_denomination_master.xml @@ -0,0 +1,180 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountConstant.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountConstant.java index 6cad12d87..85b32925e 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountConstant.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountConstant.java @@ -1,82 +1,83 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.fineract.portfolio.savings.data; - -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; -import org.apache.fineract.portfolio.savings.SavingsApiConstants; - -public class SavingsAccountConstant extends SavingsApiConstants { - - /** - * These parameters will match the class level parameters of {@link SavingsProductData}. Where possible, we try to - * get response parameters to match those of request parameters. - */ - protected static final Set SAVINGS_ACCOUNT_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(localeParamName, - dateFormatParamName, monthDayFormatParamName, staffIdParamName, isGSIM, isParentAccount, accountNoParamName, - externalIdParamName, clientIdParamName, groupIdParamName, productIdParamName, fieldOfficerIdParamName, submittedOnDateParamName, - nominalAnnualInterestRateParamName, interestCompoundingPeriodTypeParamName, interestPostingPeriodTypeParamName, - interestCalculationTypeParamName, interestCalculationDaysInYearTypeParamName, minRequiredOpeningBalanceParamName, - lockinPeriodFrequencyParamName, lockinPeriodFrequencyTypeParamName, - // withdrawalFeeAmountParamName, withdrawalFeeTypeParamName, - withdrawalFeeForTransfersParamName, feeAmountParamName, feeOnMonthDayParamName, chargesParamName, allowOverdraftParamName, - overdraftLimitParamName, minRequiredBalanceParamName, enforceMinRequiredBalanceParamName, lienAllowedParamName, - maxAllowedLienLimitParamName, nominalAnnualInterestRateOverdraftParamName, minOverdraftForInterestCalculationParamName, - withHoldTaxParamName, datatables, gsimApplicationId, gsimLastApplication, - // CBS Account fields - "accountType", "customerSegment", accountStatusParamName, modeOfOperationParamName, - specialCategoryParamName, weakerSectionParamName, - guardianNameParamName, guardianCifNumberParamName, guardianRelationshipParamName, - guardianClientIdParamName, minorDateOfBirthParamName, - introducerIdParamName, introducerNameParamName, introducerCifNumberParamName, - nomineeParamName, "nominees", linkedAccountIdParamName, linkedAccountTypeParamName, linkedAccountNoParamName, - accountFacilityConfigParamName, jointHoldersParamName, - balanceSwapOptionParamName, msmeParamName, autoDebitFacilityParamName)); - - /** - * These parameters will match the class level parameters of {@link SavingsAccountData}. Where possible, we try to - * get response parameters to match those of request parameters. - */ - - protected static final Set SAVINGS_ACCOUNT_TRANSACTION_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(localeParamName, - dateFormatParamName, transactionDateParamName, transactionAmountParamName, paymentTypeIdParamName, - transactionAccountNumberParamName, checkNumberParamName, routingCodeParamName, receiptNumberParamName, bankNumberParamName, - retailEntriesParamName, childAccountIdParamName, noteParamName, amountParamName, dateParamName, isManualTransaction, - lienTransaction, chargesPaidByData, submittedOnDateParamName, accountIdParamName, accountNoParamName)); - - protected static final Set SAVINGS_ACCOUNT_TRANSACTION_RESPONSE_DATA_PARAMETERS = new HashSet<>( - Arrays.asList(idParamName, accountNoParamName)); - - protected static final Set SAVINGS_ACCOUNT_ACTIVATION_REQUEST_DATA_PARAMETERS = new HashSet<>( - Arrays.asList(localeParamName, dateFormatParamName, activatedOnDateParamName, "openingBalance")); - - protected static final Set SAVINGS_ACCOUNT_CLOSE_REQUEST_DATA_PARAMETERS = new HashSet<>( - Arrays.asList(localeParamName, dateFormatParamName, closedOnDateParamName, noteParamName, paymentTypeIdParamName, - withdrawBalanceParamName, transactionAccountNumberParamName, checkNumberParamName, routingCodeParamName, - receiptNumberParamName, bankNumberParamName, postInterestValidationOnClosure)); - - protected static final Set SAVINGS_ACCOUNT_CHARGES_ADD_REQUEST_DATA_PARAMETERS = new HashSet<>( - Arrays.asList(chargeIdParamName, amountParamName, dueAsOfDateParamName, dateFormatParamName, localeParamName, - feeOnMonthDayParamName, monthDayFormatParamName, feeIntervalParamName)); - - protected static final Set SAVINGS_ACCOUNT_CHARGES_PAY_CHARGE_REQUEST_DATA_PARAMETERS = new HashSet<>( - Arrays.asList(amountParamName, dueAsOfDateParamName, dateFormatParamName, localeParamName, noteParamName)); - -} +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.savings.data; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import org.apache.fineract.portfolio.savings.SavingsApiConstants; + +public class SavingsAccountConstant extends SavingsApiConstants { + + /** + * These parameters will match the class level parameters of {@link SavingsProductData}. Where possible, we try to + * get response parameters to match those of request parameters. + */ + protected static final Set SAVINGS_ACCOUNT_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(localeParamName, + dateFormatParamName, monthDayFormatParamName, staffIdParamName, isGSIM, isParentAccount, accountNoParamName, + externalIdParamName, clientIdParamName, groupIdParamName, productIdParamName, fieldOfficerIdParamName, submittedOnDateParamName, + nominalAnnualInterestRateParamName, interestCompoundingPeriodTypeParamName, interestPostingPeriodTypeParamName, + interestCalculationTypeParamName, interestCalculationDaysInYearTypeParamName, minRequiredOpeningBalanceParamName, + lockinPeriodFrequencyParamName, lockinPeriodFrequencyTypeParamName, + // withdrawalFeeAmountParamName, withdrawalFeeTypeParamName, + withdrawalFeeForTransfersParamName, feeAmountParamName, feeOnMonthDayParamName, chargesParamName, allowOverdraftParamName, + overdraftLimitParamName, minRequiredBalanceParamName, enforceMinRequiredBalanceParamName, lienAllowedParamName, + maxAllowedLienLimitParamName, nominalAnnualInterestRateOverdraftParamName, minOverdraftForInterestCalculationParamName, + withHoldTaxParamName, datatables, gsimApplicationId, gsimLastApplication, + // CBS Account fields + "accountType", "customerSegment", accountStatusParamName, modeOfOperationParamName, + specialCategoryParamName, weakerSectionParamName, + guardianNameParamName, guardianCifNumberParamName, guardianRelationshipParamName, + guardianClientIdParamName, minorDateOfBirthParamName, + introducerIdParamName, introducerNameParamName, introducerCifNumberParamName, + nomineeParamName, "nominees", linkedAccountIdParamName, linkedAccountTypeParamName, linkedAccountNoParamName, + accountFacilityConfigParamName, jointHoldersParamName, + balanceSwapOptionParamName, msmeParamName, autoDebitFacilityParamName)); + + /** + * These parameters will match the class level parameters of {@link SavingsAccountData}. Where possible, we try to + * get response parameters to match those of request parameters. + */ + + protected static final Set SAVINGS_ACCOUNT_TRANSACTION_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(localeParamName, + dateFormatParamName, transactionDateParamName, transactionAmountParamName, paymentTypeIdParamName, + transactionAccountNumberParamName, checkNumberParamName, routingCodeParamName, receiptNumberParamName, bankNumberParamName, + retailEntriesParamName, childAccountIdParamName, noteParamName, amountParamName, dateParamName, isManualTransaction, + lienTransaction, chargesPaidByData, submittedOnDateParamName, accountIdParamName, accountNoParamName, + "denominationBreakdown")); + + protected static final Set SAVINGS_ACCOUNT_TRANSACTION_RESPONSE_DATA_PARAMETERS = new HashSet<>( + Arrays.asList(idParamName, accountNoParamName)); + + protected static final Set SAVINGS_ACCOUNT_ACTIVATION_REQUEST_DATA_PARAMETERS = new HashSet<>( + Arrays.asList(localeParamName, dateFormatParamName, activatedOnDateParamName, "openingBalance")); + + protected static final Set SAVINGS_ACCOUNT_CLOSE_REQUEST_DATA_PARAMETERS = new HashSet<>( + Arrays.asList(localeParamName, dateFormatParamName, closedOnDateParamName, noteParamName, paymentTypeIdParamName, + withdrawBalanceParamName, transactionAccountNumberParamName, checkNumberParamName, routingCodeParamName, + receiptNumberParamName, bankNumberParamName, postInterestValidationOnClosure)); + + protected static final Set SAVINGS_ACCOUNT_CHARGES_ADD_REQUEST_DATA_PARAMETERS = new HashSet<>( + Arrays.asList(chargeIdParamName, amountParamName, dueAsOfDateParamName, dateFormatParamName, localeParamName, + feeOnMonthDayParamName, monthDayFormatParamName, feeIntervalParamName)); + + protected static final Set SAVINGS_ACCOUNT_CHARGES_PAY_CHARGE_REQUEST_DATA_PARAMETERS = new HashSet<>( + Arrays.asList(amountParamName, dueAsOfDateParamName, dateFormatParamName, localeParamName, noteParamName)); + +} From 9a30ede278aea60a89611503dfbca9b143509902 Mon Sep 17 00:00:00 2001 From: Jaanu423 Date: Wed, 27 May 2026 18:36:00 +0530 Subject: [PATCH 06/10] feat: denomination save+validation for all deposit types + cash exchange backend - DenominationBreakdownWriter helper: validates breakdown total == amount and persists per-denomination rows; wired into current, FD and RD deposit flows (savings already had inline save) so all 4 deposit types now record the cash denomination breakdown. - CIF on denomination rows now sourced from the client (getCifNumber) instead of the account external id. - Allow denominationBreakdown param in the FD/RD transaction validator. - Cash Exchange backend: m_cash_exchange + m_cash_exchange_detail tables, entities, repositories and JAX-RS API with received==given validation; registered the 0013 migration in changelog-tenant. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../api/CashExchangeApiResource.java | 199 ++ .../cashexchange/domain/CashExchange.java | 57 + .../domain/CashExchangeDetail.java | 46 + .../domain/CashExchangeDetailRepository.java | 10 + .../domain/CashExchangeRepository.java | 11 + .../domain/SavingsTxnDenomination.java | 12 + .../service/DenominationBreakdownWriter.java | 232 ++ ...epositAccountTransactionDataValidator.java | 514 +-- ...ntTransactionWritePlatformServiceImpl.java | 15 + ...WritePlatformServiceJpaRepositoryImpl.java | 2820 +++++++++-------- ...WritePlatformServiceJpaRepositoryImpl.java | 49 +- .../savings/starter/SavingsConfiguration.java | 11 +- .../db/changelog/tenant/changelog-tenant.xml | 1 + .../tenant/parts/0012_denomination_master.xml | 24 + .../tenant/parts/0013_cash_exchange.xml | 52 + 15 files changed, 2355 insertions(+), 1698 deletions(-) create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/cashexchange/api/CashExchangeApiResource.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/cashexchange/domain/CashExchange.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/cashexchange/domain/CashExchangeDetail.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/cashexchange/domain/CashExchangeDetailRepository.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/cashexchange/domain/CashExchangeRepository.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/service/DenominationBreakdownWriter.java create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0013_cash_exchange.xml diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashexchange/api/CashExchangeApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashexchange/api/CashExchangeApiResource.java new file mode 100644 index 000000000..c06a5d346 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashexchange/api/CashExchangeApiResource.java @@ -0,0 +1,199 @@ +package org.apache.fineract.portfolio.cashexchange.api; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.portfolio.cashexchange.domain.CashExchange; +import org.apache.fineract.portfolio.cashexchange.domain.CashExchangeDetail; +import org.apache.fineract.portfolio.cashexchange.domain.CashExchangeDetailRepository; +import org.apache.fineract.portfolio.cashexchange.domain.CashExchangeRepository; +import org.apache.fineract.useradministration.domain.AppUser; +import org.springframework.stereotype.Component; + +@Path("/v1/cashexchange") +@Component +@RequiredArgsConstructor +@Consumes({ MediaType.APPLICATION_JSON }) +@Produces({ MediaType.APPLICATION_JSON }) +public class CashExchangeApiResource { + + private final CashExchangeRepository repository; + private final CashExchangeDetailRepository detailRepository; + private final PlatformSecurityContext context; + private final Gson gson = new Gson(); + + @POST + public String create(String body) { + JsonObject json = JsonParser.parseString(body).getAsJsonObject(); + + final Long accountId = json.has("accountId") && !json.get("accountId").isJsonNull() ? json.get("accountId").getAsLong() : null; + final String accountNo = json.has("accountNo") && !json.get("accountNo").isJsonNull() ? json.get("accountNo").getAsString() + : null; + final Long clientId = json.has("clientId") && !json.get("clientId").isJsonNull() ? json.get("clientId").getAsLong() : null; + final LocalDate exchangeDate = json.has("exchangeDate") && !json.get("exchangeDate").isJsonNull() + ? LocalDate.parse(json.get("exchangeDate").getAsString()) + : null; + final String note = json.has("note") && !json.get("note").isJsonNull() ? json.get("note").getAsString() : null; + + final JsonArray received = json.has("received") && json.get("received").isJsonArray() ? json.getAsJsonArray("received") + : new JsonArray(); + final JsonArray given = json.has("given") && json.get("given").isJsonArray() ? json.getAsJsonArray("given") : new JsonArray(); + + final BigDecimal receivedTotal = sumTotal(received); + final BigDecimal givenTotal = sumTotal(given); + + if (!hasPositiveCount(received) || !hasPositiveCount(given)) { + throw new PlatformDataIntegrityException("error.msg.cashexchange.empty", + "Both received and given denominations are required."); + } + if (receivedTotal.compareTo(givenTotal) != 0) { + throw new PlatformDataIntegrityException("error.msg.cashexchange.mismatch", + "Received total (" + receivedTotal + ") must equal given total (" + givenTotal + ")."); + } + if (receivedTotal.compareTo(BigDecimal.ZERO) <= 0) { + throw new PlatformDataIntegrityException("error.msg.cashexchange.empty", + "Both received and given denominations are required."); + } + + CashExchange header = new CashExchange(); + header.setAccountId(accountId); + header.setAccountNo(accountNo); + header.setClientId(clientId); + header.setExchangeAmount(receivedTotal); + header.setExchangeDate(exchangeDate); + header.setNote(note); + header.setCreatedAt(LocalDateTime.now()); + try { + AppUser user = this.context.authenticatedUser(); + if (user != null) { + header.setPerformedBy(user.getId()); + header.setPerformedByUsername(user.getUsername()); + } + } catch (Exception e) { + // resolving the current user must never block recording the exchange + } + CashExchange saved = repository.save(header); + + saveDetails(saved.getId(), "RECEIVED", received); + saveDetails(saved.getId(), "GIVEN", given); + + JsonObject result = new JsonObject(); + result.addProperty("resourceId", saved.getId()); + return gson.toJson(result); + } + + @GET + public String getAll(@QueryParam("accountId") Long accountId) { + List list = accountId != null ? repository.findByAccountIdOrderByCreatedAtDesc(accountId) + : repository.findAllByOrderByCreatedAtDesc(); + return gson.toJson(list.stream().map(this::toMap).collect(Collectors.toList())); + } + + @GET + @Path("{id}") + public String getOne(@PathParam("id") Long id) { + CashExchange header = repository.findById(id) + .orElseThrow(() -> new RuntimeException("Cash exchange not found: " + id)); + Map map = toMap(header); + List> details = detailRepository.findByExchangeId(id).stream().map(this::detailToMap) + .collect(Collectors.toList()); + map.put("details", details); + return gson.toJson(map); + } + + private BigDecimal sumTotal(JsonArray items) { + BigDecimal total = BigDecimal.ZERO; + for (JsonElement el : items) { + JsonObject item = el.getAsJsonObject(); + int value = item.has("denominationValue") && !item.get("denominationValue").isJsonNull() + ? item.get("denominationValue").getAsInt() + : 0; + int count = item.has("count") && !item.get("count").isJsonNull() ? item.get("count").getAsInt() : 0; + if (count > 0) { + total = total.add(BigDecimal.valueOf((long) value * count)); + } + } + return total; + } + + private boolean hasPositiveCount(JsonArray items) { + for (JsonElement el : items) { + JsonObject item = el.getAsJsonObject(); + int count = item.has("count") && !item.get("count").isJsonNull() ? item.get("count").getAsInt() : 0; + if (count > 0) { + return true; + } + } + return false; + } + + private void saveDetails(Long exchangeId, String direction, JsonArray items) { + for (JsonElement el : items) { + JsonObject item = el.getAsJsonObject(); + int count = item.has("count") && !item.get("count").isJsonNull() ? item.get("count").getAsInt() : 0; + if (count <= 0) { + continue; + } + int value = item.has("denominationValue") && !item.get("denominationValue").isJsonNull() + ? item.get("denominationValue").getAsInt() + : 0; + Long denominationId = item.has("denominationId") && !item.get("denominationId").isJsonNull() + ? item.get("denominationId").getAsLong() + : null; + CashExchangeDetail detail = new CashExchangeDetail(); + detail.setExchangeId(exchangeId); + detail.setDirection(direction); + detail.setDenominationId(denominationId); + detail.setDenominationValue(value); + detail.setCount(count); + detail.setTotalAmount(BigDecimal.valueOf((long) value * count)); + detailRepository.save(detail); + } + } + + private Map toMap(CashExchange e) { + Map map = new LinkedHashMap<>(); + map.put("id", e.getId()); + map.put("accountId", e.getAccountId()); + map.put("accountNo", e.getAccountNo()); + map.put("clientId", e.getClientId()); + map.put("exchangeAmount", e.getExchangeAmount()); + map.put("exchangeDate", e.getExchangeDate() != null ? e.getExchangeDate().toString() : null); + map.put("note", e.getNote()); + map.put("performedBy", e.getPerformedBy()); + map.put("performedByUsername", e.getPerformedByUsername()); + map.put("createdAt", e.getCreatedAt() != null ? e.getCreatedAt().toString() : null); + return map; + } + + private Map detailToMap(CashExchangeDetail d) { + Map map = new LinkedHashMap<>(); + map.put("id", d.getId()); + map.put("direction", d.getDirection()); + map.put("denominationId", d.getDenominationId()); + map.put("denominationValue", d.getDenominationValue()); + map.put("count", d.getCount()); + map.put("totalAmount", d.getTotalAmount()); + return map; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashexchange/domain/CashExchange.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashexchange/domain/CashExchange.java new file mode 100644 index 000000000..586772c5b --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashexchange/domain/CashExchange.java @@ -0,0 +1,57 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. + */ +package org.apache.fineract.portfolio.cashexchange.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Table(name = "m_cash_exchange") +@Getter +@Setter +@NoArgsConstructor +public class CashExchange { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "account_id") + private Long accountId; + + @Column(name = "account_no", length = 50) + private String accountNo; + + @Column(name = "client_id") + private Long clientId; + + @Column(name = "exchange_amount", nullable = false) + private BigDecimal exchangeAmount; + + @Column(name = "exchange_date") + private LocalDate exchangeDate; + + @Column(name = "note", length = 500) + private String note; + + @Column(name = "performed_by") + private Long performedBy; + + @Column(name = "performed_by_username", length = 100) + private String performedByUsername; + + @Column(name = "created_at") + private LocalDateTime createdAt; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashexchange/domain/CashExchangeDetail.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashexchange/domain/CashExchangeDetail.java new file mode 100644 index 000000000..ad4696058 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashexchange/domain/CashExchangeDetail.java @@ -0,0 +1,46 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. + */ +package org.apache.fineract.portfolio.cashexchange.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.math.BigDecimal; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Table(name = "m_cash_exchange_detail") +@Getter +@Setter +@NoArgsConstructor +public class CashExchangeDetail { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "exchange_id", nullable = false) + private Long exchangeId; + + @Column(name = "direction", nullable = false, length = 10) + private String direction; + + @Column(name = "denomination_id") + private Long denominationId; + + @Column(name = "denomination_value", nullable = false) + private Integer denominationValue; + + @Column(name = "count", nullable = false) + private Integer count; + + @Column(name = "total_amount", nullable = false) + private BigDecimal totalAmount; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashexchange/domain/CashExchangeDetailRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashexchange/domain/CashExchangeDetailRepository.java new file mode 100644 index 000000000..4587720c9 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashexchange/domain/CashExchangeDetailRepository.java @@ -0,0 +1,10 @@ +package org.apache.fineract.portfolio.cashexchange.domain; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface CashExchangeDetailRepository extends JpaRepository { + List findByExchangeId(Long exchangeId); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashexchange/domain/CashExchangeRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashexchange/domain/CashExchangeRepository.java new file mode 100644 index 000000000..3cffef6c1 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashexchange/domain/CashExchangeRepository.java @@ -0,0 +1,11 @@ +package org.apache.fineract.portfolio.cashexchange.domain; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface CashExchangeRepository extends JpaRepository { + List findAllByOrderByCreatedAtDesc(); + List findByAccountIdOrderByCreatedAtDesc(Long accountId); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/domain/SavingsTxnDenomination.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/domain/SavingsTxnDenomination.java index 8db21d345..fedd02652 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/domain/SavingsTxnDenomination.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/domain/SavingsTxnDenomination.java @@ -78,4 +78,16 @@ public class SavingsTxnDenomination { @Column(name = "created_at") private LocalDateTime createdAt; + + /** "DEPOSIT" or "WITHDRAWAL" — identifies the direction of the cash flow. */ + @Column(name = "txn_type", nullable = false, length = 20) + private String txnType; + + /** ID of the logged-in teller / officer who performed the transaction. */ + @Column(name = "officer_id") + private Long officerId; + + /** Username of the teller / officer who performed the transaction. */ + @Column(name = "officer_username", length = 100) + private String officerUsername; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/service/DenominationBreakdownWriter.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/service/DenominationBreakdownWriter.java new file mode 100644 index 000000000..23c4fb3db --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/service/DenominationBreakdownWriter.java @@ -0,0 +1,232 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. + */ +package org.apache.fineract.portfolio.denominationmaster.service; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import lombok.RequiredArgsConstructor; +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.exception.PlatformApiDataValidationException; +import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.portfolio.denominationmaster.domain.SavingsTxnDenomination; +import org.apache.fineract.portfolio.denominationmaster.domain.SavingsTxnDenominationRepository; +import org.apache.fineract.useradministration.domain.AppUser; +import org.springframework.stereotype.Service; + +/** + * Validates and persists the per-transaction cash denomination breakdown sent by the UI + * on both deposits and withdrawals. + */ +@Service +@RequiredArgsConstructor +public class DenominationBreakdownWriter { + + private final SavingsTxnDenominationRepository repository; + private final PlatformSecurityContext context; + + // ── Public API ──────────────────────────────────────────────────────────── + + /** + * Full validation of the denomination breakdown: + *

    + *
  1. Each item must have a non-null {@code denominationId}.
  2. + *
  3. Each item's {@code count} must be > 0.
  4. + *
  5. Each item's {@code denominationValue} (or {@code denomination}) must be > 0.
  6. + *
  7. The grand total (sum of value × count) must equal {@code expectedAmount}.
  8. + *
+ * No-op when no breakdown is provided. + * + * @param command the JSON command containing the denominationBreakdown array + * @param expectedAmount the transaction amount that the breakdown must sum to + * @param txnType "DEPOSIT" or "WITHDRAWAL" — used in error messages + */ + public void validateBreakdown(final JsonCommand command, final BigDecimal expectedAmount, final String txnType) { + if (command == null || !command.parameterExists("denominationBreakdown")) { + return; + } + final JsonArray breakdown = command.arrayOfParameterNamed("denominationBreakdown"); + if (breakdown == null || breakdown.size() == 0) { + return; + } + + final List errors = new ArrayList<>(); + final DataValidatorBuilder validator = new DataValidatorBuilder(errors).resource("denominationBreakdown"); + BigDecimal computedTotal = BigDecimal.ZERO; + final String label = (txnType != null && !txnType.isBlank()) ? txnType.toUpperCase() : "TRANSACTION"; + + for (int i = 0; i < breakdown.size(); i++) { + final JsonObject obj = breakdown.get(i).getAsJsonObject(); + final String itemPrefix = "denominationBreakdown[" + i + "]"; + + // 1. denominationId is required + if (!obj.has("denominationId") || obj.get("denominationId").isJsonNull()) { + validator.reset().parameter(itemPrefix + ".denominationId") + .value(null) + .failWithCode("required", "denominationId is required for each denomination item."); + continue; // cannot compute further without ID + } + final long denominationId = obj.get("denominationId").getAsLong(); + if (denominationId <= 0) { + validator.reset().parameter(itemPrefix + ".denominationId") + .value(denominationId) + .failWithCode("invalid", "denominationId must be a positive number."); + } + + // 2. count is required and must be > 0 + if (!obj.has("count") || obj.get("count").isJsonNull()) { + validator.reset().parameter(itemPrefix + ".count") + .value(null) + .failWithCode("required", "count is required for each denomination item."); + continue; + } + final int count = obj.get("count").getAsInt(); + if (count <= 0) { + // count = 0 is silently skipped (UI may send zero for unselected rows) + continue; + } + + // 3. denominationValue (or denomination) must be present and > 0 + int denominationValue = 0; + if (obj.has("denominationValue") && !obj.get("denominationValue").isJsonNull()) { + denominationValue = obj.get("denominationValue").getAsInt(); + } else if (obj.has("denomination") && !obj.get("denomination").isJsonNull()) { + denominationValue = obj.get("denomination").getAsInt(); + } + if (denominationValue <= 0) { + validator.reset().parameter(itemPrefix + ".denominationValue") + .value(denominationValue) + .failWithCode("invalid", + "denominationValue must be greater than zero for denomination item " + i + "."); + continue; + } + + // 4. Accumulate total: value × count + computedTotal = computedTotal.add(BigDecimal.valueOf((long) denominationValue * count)); + } + + // Throw item-level validation errors first + if (!errors.isEmpty()) { + throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", + "Denomination breakdown validation errors exist.", errors); + } + + // 5. Grand total must match the transaction amount + if (expectedAmount != null && computedTotal.compareTo(expectedAmount) != 0) { + throw new PlatformDataIntegrityException( + "error.msg.denomination.total.mismatch", + label + " denomination breakdown total (" + computedTotal + + ") does not match the transaction amount (" + expectedAmount + + "). Please re-check the note/coin counts."); + } + } + + /** + * Convenience wrapper kept for backward-compatibility with any callers that do not + * yet pass a {@code txnType}. Delegates to {@link #validateBreakdown}. + */ + public void validateTotal(final JsonCommand command, final BigDecimal expectedAmount) { + validateBreakdown(command, expectedAmount, "TRANSACTION"); + } + + /** + * Persist each valid denomination row for the given transaction. + *

+ * Items with {@code count <= 0} are silently skipped (UI sends zero for + * unselected rows). Items missing {@code denominationId} are also skipped + * (they should have already been rejected by {@link #validateBreakdown}). + *

+ * The logged-in officer (teller) is automatically resolved from the + * security context; if resolution fails (e.g. batch job) the officer fields + * are left {@code null}. + * + * @param command the JSON command containing the denominationBreakdown array + * @param txnId ID of the saved savings / current / RD / FD transaction + * @param savingsAccountId savings / current / RD / FD account ID + * @param accountNo account number string + * @param clientId client ID (may be null for group accounts) + * @param cifNo CIF number (may be null) + * @param txnDate transaction date + * @param txnType "DEPOSIT" or "WITHDRAWAL" + */ + public void save(final JsonCommand command, final Long txnId, final Long savingsAccountId, final String accountNo, + final Long clientId, final String cifNo, final LocalDate txnDate, final String txnType) { + + if (command == null || txnId == null || !command.parameterExists("denominationBreakdown")) { + return; + } + final JsonArray breakdown = command.arrayOfParameterNamed("denominationBreakdown"); + if (breakdown == null || breakdown.size() == 0) { + return; + } + + // Resolve the officer / teller from the security context + Long officerId = null; + String officerUsername = null; + try { + final AppUser user = this.context.authenticatedUser(); + if (user != null) { + officerId = user.getId(); + officerUsername = user.getUsername(); + } + } catch (final Exception ignored) { + // If user cannot be resolved (e.g. batch / async job), leave officer fields null + } + + final String resolvedTxnType = (txnType != null && !txnType.isBlank()) ? txnType.toUpperCase() : "DEPOSIT"; + + for (final JsonElement entry : breakdown) { + final JsonObject obj = entry.getAsJsonObject(); + + // denominationId is mandatory (NOT NULL in DB) + if (!obj.has("denominationId") || obj.get("denominationId").isJsonNull() || !obj.has("count")) { + continue; + } + final int count = obj.get("count").getAsInt(); + if (count <= 0) { + continue; // zero-count rows sent by UI are silently ignored + } + + final long denominationId = obj.get("denominationId").getAsLong(); + + // Resolve denomination face value + int denominationValue = 0; + if (obj.has("denominationValue") && !obj.get("denominationValue").isJsonNull()) { + denominationValue = obj.get("denominationValue").getAsInt(); + } else if (obj.has("denomination") && !obj.get("denomination").isJsonNull()) { + denominationValue = obj.get("denomination").getAsInt(); + } + + final BigDecimal lineTotal = BigDecimal.valueOf((long) denominationValue * count); + + final SavingsTxnDenomination row = SavingsTxnDenomination.builder() + .txnId(txnId) + .savingsAccountId(savingsAccountId) + .accountNo(accountNo) + .clientId(clientId) + .cifNo(cifNo) + .denominationId(denominationId) + .denominationValue(denominationValue) + .count(count) + .totalAmount(lineTotal) + .txnDate(txnDate) + .createdAt(LocalDateTime.now()) + .txnType(resolvedTxnType) + .officerId(officerId) + .officerUsername(officerUsername) + .build(); + + this.repository.save(row); + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/DepositAccountTransactionDataValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/DepositAccountTransactionDataValidator.java index fa09ace7b..03b69a1da 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/DepositAccountTransactionDataValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/DepositAccountTransactionDataValidator.java @@ -1,257 +1,257 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.fineract.portfolio.savings.data; - -import static org.apache.fineract.portfolio.savings.DepositsApiConstants.activatedOnDateParamName; -import static org.apache.fineract.portfolio.savings.DepositsApiConstants.bankNumberParamName; -import static org.apache.fineract.portfolio.savings.DepositsApiConstants.checkNumberParamName; -import static org.apache.fineract.portfolio.savings.DepositsApiConstants.closedOnDateParamName; -import static org.apache.fineract.portfolio.savings.DepositsApiConstants.onAccountClosureIdParamName; -import static org.apache.fineract.portfolio.savings.DepositsApiConstants.paymentTypeIdParamName; -import static org.apache.fineract.portfolio.savings.DepositsApiConstants.receiptNumberParamName; -import static org.apache.fineract.portfolio.savings.DepositsApiConstants.routingCodeParamName; -import static org.apache.fineract.portfolio.savings.DepositsApiConstants.toSavingsAccountIdParamName; -import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transactionAccountNumberParamName; -import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transactionAmountParamName; -import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transactionDateParamName; - -import com.google.gson.JsonElement; -import com.google.gson.reflect.TypeToken; -import java.lang.reflect.Type; -import java.math.BigDecimal; -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import org.apache.commons.lang3.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.exception.InvalidJsonException; -import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; -import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; -import org.apache.fineract.portfolio.savings.DepositAccountOnClosureType; -import org.apache.fineract.portfolio.savings.DepositAccountType; -import org.apache.fineract.portfolio.savings.DepositsApiConstants; -import org.apache.fineract.portfolio.savings.SavingsApiConstants; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -@Component -public class DepositAccountTransactionDataValidator { - - private final FromJsonHelper fromApiJsonHelper; - - private static final Set DEPOSIT_ACCOUNT_TRANSACTION_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList( - DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName, transactionDateParamName, - transactionAmountParamName, paymentTypeIdParamName, transactionAccountNumberParamName, checkNumberParamName, - routingCodeParamName, receiptNumberParamName, bankNumberParamName, DepositsApiConstants.amountParamName, - DepositsApiConstants.accountIdParamName, DepositsApiConstants.dateParamName, DepositsApiConstants.submittedOnDateParamName, - DepositsApiConstants.lienTransaction, DepositsApiConstants.isManualTransaction, DepositsApiConstants.chargesPaidByData, - DepositsApiConstants.accountNoParamName, DepositsApiConstants.noteParamName)); - - private static final Set DEPOSIT_ACCOUNT_RECOMMENDED_DEPOSIT_AMOUNT_UPDATE_REQUEST_DATA_PARAMETERS = new HashSet<>( - Arrays.asList(DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName, - DepositsApiConstants.mandatoryRecommendedDepositAmountParamName, DepositsApiConstants.effectiveDateParamName)); - - private static final Set DEPOSIT_ACCOUNT_CLOSE_REQUEST_DATA_PARAMETERS = new HashSet<>( - Arrays.asList(DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName, closedOnDateParamName, - DepositsApiConstants.noteParamName, onAccountClosureIdParamName, paymentTypeIdParamName, - transactionAccountNumberParamName, checkNumberParamName, routingCodeParamName, receiptNumberParamName, - bankNumberParamName, DepositsApiConstants.transferDescriptionParamName, toSavingsAccountIdParamName)); - - private static final Set DEPOSIT_ACCOUNT_PRE_MATURE_CALCULATION_REQUEST_DATA_PARAMETERS = new HashSet<>( - Arrays.asList(DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName, closedOnDateParamName)); - - @Autowired - public DepositAccountTransactionDataValidator(final FromJsonHelper fromApiJsonHelper) { - this.fromApiJsonHelper = fromApiJsonHelper; - } - - public void validate(final JsonCommand command, DepositAccountType depositAccountType) { - - final String json = command.json(); - - if (StringUtils.isBlank(json)) { - throw new InvalidJsonException(); - } - - final Type typeOfMap = new TypeToken>() {}.getType(); - this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, DEPOSIT_ACCOUNT_TRANSACTION_REQUEST_DATA_PARAMETERS); - - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(depositAccountType.resourceName()); - - final JsonElement element = command.parsedJson(); - - final LocalDate transactionDate = this.fromApiJsonHelper.extractLocalDateNamed(transactionDateParamName, element); - baseDataValidator.reset().parameter(transactionDateParamName).value(transactionDate).notNull(); - - final BigDecimal transactionAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(transactionAmountParamName, element); - baseDataValidator.reset().parameter(transactionAmountParamName).value(transactionAmount).notNull().positiveAmount(); - - // Validate all string payment detail fields for max length - final Integer paymentTypeId = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(paymentTypeIdParamName, element); - baseDataValidator.reset().parameter(paymentTypeIdParamName).value(paymentTypeId).ignoreIfNull().integerGreaterThanZero(); - final Set paymentDetailParameters = new HashSet<>(Arrays.asList(transactionAccountNumberParamName, checkNumberParamName, - routingCodeParamName, receiptNumberParamName, bankNumberParamName)); - for (final String paymentDetailParameterName : paymentDetailParameters) { - final String paymentDetailParameterValue = this.fromApiJsonHelper.extractStringNamed(paymentDetailParameterName, element); - baseDataValidator.reset().parameter(paymentDetailParameterName).value(paymentDetailParameterValue).ignoreIfNull() - .notExceedingLengthOf(50); - } - - throwExceptionIfValidationWarningsExist(dataValidationErrors); - } - - public void validateDepositAmountUpdate(final JsonCommand command) { - final String json = command.json(); - - if (StringUtils.isBlank(json)) { - throw new InvalidJsonException(); - } - - final Type typeOfMap = new TypeToken>() {}.getType(); - this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, - DEPOSIT_ACCOUNT_RECOMMENDED_DEPOSIT_AMOUNT_UPDATE_REQUEST_DATA_PARAMETERS); - - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME); - - final JsonElement element = command.parsedJson(); - - final LocalDate effectiveDate = this.fromApiJsonHelper.extractLocalDateNamed(DepositsApiConstants.effectiveDateParamName, element); - baseDataValidator.reset().parameter(DepositsApiConstants.effectiveDateParamName).value(effectiveDate).notNull(); - - final BigDecimal mandatoryRecommendedDepositAmount = this.fromApiJsonHelper - .extractBigDecimalWithLocaleNamed(DepositsApiConstants.mandatoryRecommendedDepositAmountParamName, element); - baseDataValidator.reset().parameter(DepositsApiConstants.mandatoryRecommendedDepositAmountParamName) - .value(mandatoryRecommendedDepositAmount).notNull().positiveAmount(); - - throwExceptionIfValidationWarningsExist(dataValidationErrors); - } - - public void validateActivation(final JsonCommand command) { - final String json = command.json(); - - if (StringUtils.isBlank(json)) { - throw new InvalidJsonException(); - } - - final Type typeOfMap = new TypeToken>() {}.getType(); - this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, - SavingsAccountConstant.SAVINGS_ACCOUNT_ACTIVATION_REQUEST_DATA_PARAMETERS); - - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME); - - final JsonElement element = command.parsedJson(); - - final LocalDate activationDate = this.fromApiJsonHelper.extractLocalDateNamed(activatedOnDateParamName, element); - baseDataValidator.reset().parameter(activatedOnDateParamName).value(activationDate).notNull(); - - throwExceptionIfValidationWarningsExist(dataValidationErrors); - } - - public void validatePreMatureAmountCalculation(final String json, final DepositAccountType depositAccountType) { - - if (StringUtils.isBlank(json)) { - throw new InvalidJsonException(); - } - - final Type typeOfMap = new TypeToken>() {}.getType(); - this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, - DEPOSIT_ACCOUNT_PRE_MATURE_CALCULATION_REQUEST_DATA_PARAMETERS); - - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(depositAccountType.resourceName()); - - final JsonElement element = this.fromApiJsonHelper.parse(json); - - final LocalDate closeDate = this.fromApiJsonHelper.extractLocalDateNamed(closedOnDateParamName, element); - baseDataValidator.reset().parameter(closedOnDateParamName).value(closeDate).notNull(); - - throwExceptionIfValidationWarningsExist(dataValidationErrors); - } - - public void validateClosing(final JsonCommand command, DepositAccountType depositAccountType, final boolean isPreMatureClose) { - final String json = command.json(); - - if (StringUtils.isBlank(json)) { - throw new InvalidJsonException(); - } - - final Type typeOfMap = new TypeToken>() {}.getType(); - this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, DEPOSIT_ACCOUNT_CLOSE_REQUEST_DATA_PARAMETERS); - - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(depositAccountType.resourceName()); - - final JsonElement element = command.parsedJson(); - - final LocalDate activationDate = this.fromApiJsonHelper.extractLocalDateNamed(closedOnDateParamName, element); - baseDataValidator.reset().parameter(closedOnDateParamName).value(activationDate).notNull(); - - final Integer onAccountClosureId = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(onAccountClosureIdParamName, element); - baseDataValidator.reset().parameter(onAccountClosureIdParamName).value(onAccountClosureId).notBlank() - .isOneOfTheseValues(DepositAccountOnClosureType.integerValues()); - - if (onAccountClosureId != null) { - final DepositAccountOnClosureType accountOnClosureType = DepositAccountOnClosureType.fromInt(onAccountClosureId); - if (accountOnClosureType.isTransferToSavings()) { - final Long toSavingsAccountId = this.fromApiJsonHelper.extractLongNamed(toSavingsAccountIdParamName, element); - baseDataValidator.reset().parameter(toSavingsAccountIdParamName).value(toSavingsAccountId) - .cantBeBlankWhenParameterProvidedIs(onAccountClosureIdParamName, - DepositAccountOnClosureType.fromInt(onAccountClosureId).getCode()); - } else if (accountOnClosureType.isReinvest() && isPreMatureClose) { - baseDataValidator.reset().parameter(onAccountClosureIdParamName).value(onAccountClosureId) - .failWithCode("reinvest.not.allowed", "Re-Invest is not supported for account pre mature close"); - } - } - - // Validate all string payment detail fields for max length - final Integer paymentTypeId = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(paymentTypeIdParamName, element); - baseDataValidator.reset().parameter(paymentTypeIdParamName).value(paymentTypeId).ignoreIfNull().integerGreaterThanZero(); - final Set paymentDetailParameters = new HashSet<>(Arrays.asList(transactionAccountNumberParamName, checkNumberParamName, - routingCodeParamName, receiptNumberParamName, bankNumberParamName)); - for (final String paymentDetailParameterName : paymentDetailParameters) { - final String paymentDetailParameterValue = this.fromApiJsonHelper.extractStringNamed(paymentDetailParameterName, element); - baseDataValidator.reset().parameter(paymentDetailParameterName).value(paymentDetailParameterValue).ignoreIfNull() - .notExceedingLengthOf(50); - } - - throwExceptionIfValidationWarningsExist(dataValidationErrors); - } - - private void throwExceptionIfValidationWarningsExist(final List dataValidationErrors) { - if (!dataValidationErrors.isEmpty()) { - // - throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", - dataValidationErrors); - } - } -} +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.savings.data; + +import static org.apache.fineract.portfolio.savings.DepositsApiConstants.activatedOnDateParamName; +import static org.apache.fineract.portfolio.savings.DepositsApiConstants.bankNumberParamName; +import static org.apache.fineract.portfolio.savings.DepositsApiConstants.checkNumberParamName; +import static org.apache.fineract.portfolio.savings.DepositsApiConstants.closedOnDateParamName; +import static org.apache.fineract.portfolio.savings.DepositsApiConstants.onAccountClosureIdParamName; +import static org.apache.fineract.portfolio.savings.DepositsApiConstants.paymentTypeIdParamName; +import static org.apache.fineract.portfolio.savings.DepositsApiConstants.receiptNumberParamName; +import static org.apache.fineract.portfolio.savings.DepositsApiConstants.routingCodeParamName; +import static org.apache.fineract.portfolio.savings.DepositsApiConstants.toSavingsAccountIdParamName; +import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transactionAccountNumberParamName; +import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transactionAmountParamName; +import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transactionDateParamName; + +import com.google.gson.JsonElement; +import com.google.gson.reflect.TypeToken; +import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.apache.commons.lang3.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.exception.InvalidJsonException; +import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.portfolio.savings.DepositAccountOnClosureType; +import org.apache.fineract.portfolio.savings.DepositAccountType; +import org.apache.fineract.portfolio.savings.DepositsApiConstants; +import org.apache.fineract.portfolio.savings.SavingsApiConstants; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class DepositAccountTransactionDataValidator { + + private final FromJsonHelper fromApiJsonHelper; + + private static final Set DEPOSIT_ACCOUNT_TRANSACTION_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList( + DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName, transactionDateParamName, + transactionAmountParamName, paymentTypeIdParamName, transactionAccountNumberParamName, checkNumberParamName, + routingCodeParamName, receiptNumberParamName, bankNumberParamName, DepositsApiConstants.amountParamName, + DepositsApiConstants.accountIdParamName, DepositsApiConstants.dateParamName, DepositsApiConstants.submittedOnDateParamName, + DepositsApiConstants.lienTransaction, DepositsApiConstants.isManualTransaction, DepositsApiConstants.chargesPaidByData, + DepositsApiConstants.accountNoParamName, DepositsApiConstants.noteParamName, "denominationBreakdown")); + + private static final Set DEPOSIT_ACCOUNT_RECOMMENDED_DEPOSIT_AMOUNT_UPDATE_REQUEST_DATA_PARAMETERS = new HashSet<>( + Arrays.asList(DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName, + DepositsApiConstants.mandatoryRecommendedDepositAmountParamName, DepositsApiConstants.effectiveDateParamName)); + + private static final Set DEPOSIT_ACCOUNT_CLOSE_REQUEST_DATA_PARAMETERS = new HashSet<>( + Arrays.asList(DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName, closedOnDateParamName, + DepositsApiConstants.noteParamName, onAccountClosureIdParamName, paymentTypeIdParamName, + transactionAccountNumberParamName, checkNumberParamName, routingCodeParamName, receiptNumberParamName, + bankNumberParamName, DepositsApiConstants.transferDescriptionParamName, toSavingsAccountIdParamName)); + + private static final Set DEPOSIT_ACCOUNT_PRE_MATURE_CALCULATION_REQUEST_DATA_PARAMETERS = new HashSet<>( + Arrays.asList(DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName, closedOnDateParamName)); + + @Autowired + public DepositAccountTransactionDataValidator(final FromJsonHelper fromApiJsonHelper) { + this.fromApiJsonHelper = fromApiJsonHelper; + } + + public void validate(final JsonCommand command, DepositAccountType depositAccountType) { + + final String json = command.json(); + + if (StringUtils.isBlank(json)) { + throw new InvalidJsonException(); + } + + final Type typeOfMap = new TypeToken>() {}.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, DEPOSIT_ACCOUNT_TRANSACTION_REQUEST_DATA_PARAMETERS); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(depositAccountType.resourceName()); + + final JsonElement element = command.parsedJson(); + + final LocalDate transactionDate = this.fromApiJsonHelper.extractLocalDateNamed(transactionDateParamName, element); + baseDataValidator.reset().parameter(transactionDateParamName).value(transactionDate).notNull(); + + final BigDecimal transactionAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(transactionAmountParamName, element); + baseDataValidator.reset().parameter(transactionAmountParamName).value(transactionAmount).notNull().positiveAmount(); + + // Validate all string payment detail fields for max length + final Integer paymentTypeId = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(paymentTypeIdParamName, element); + baseDataValidator.reset().parameter(paymentTypeIdParamName).value(paymentTypeId).ignoreIfNull().integerGreaterThanZero(); + final Set paymentDetailParameters = new HashSet<>(Arrays.asList(transactionAccountNumberParamName, checkNumberParamName, + routingCodeParamName, receiptNumberParamName, bankNumberParamName)); + for (final String paymentDetailParameterName : paymentDetailParameters) { + final String paymentDetailParameterValue = this.fromApiJsonHelper.extractStringNamed(paymentDetailParameterName, element); + baseDataValidator.reset().parameter(paymentDetailParameterName).value(paymentDetailParameterValue).ignoreIfNull() + .notExceedingLengthOf(50); + } + + throwExceptionIfValidationWarningsExist(dataValidationErrors); + } + + public void validateDepositAmountUpdate(final JsonCommand command) { + final String json = command.json(); + + if (StringUtils.isBlank(json)) { + throw new InvalidJsonException(); + } + + final Type typeOfMap = new TypeToken>() {}.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, + DEPOSIT_ACCOUNT_RECOMMENDED_DEPOSIT_AMOUNT_UPDATE_REQUEST_DATA_PARAMETERS); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME); + + final JsonElement element = command.parsedJson(); + + final LocalDate effectiveDate = this.fromApiJsonHelper.extractLocalDateNamed(DepositsApiConstants.effectiveDateParamName, element); + baseDataValidator.reset().parameter(DepositsApiConstants.effectiveDateParamName).value(effectiveDate).notNull(); + + final BigDecimal mandatoryRecommendedDepositAmount = this.fromApiJsonHelper + .extractBigDecimalWithLocaleNamed(DepositsApiConstants.mandatoryRecommendedDepositAmountParamName, element); + baseDataValidator.reset().parameter(DepositsApiConstants.mandatoryRecommendedDepositAmountParamName) + .value(mandatoryRecommendedDepositAmount).notNull().positiveAmount(); + + throwExceptionIfValidationWarningsExist(dataValidationErrors); + } + + public void validateActivation(final JsonCommand command) { + final String json = command.json(); + + if (StringUtils.isBlank(json)) { + throw new InvalidJsonException(); + } + + final Type typeOfMap = new TypeToken>() {}.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, + SavingsAccountConstant.SAVINGS_ACCOUNT_ACTIVATION_REQUEST_DATA_PARAMETERS); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME); + + final JsonElement element = command.parsedJson(); + + final LocalDate activationDate = this.fromApiJsonHelper.extractLocalDateNamed(activatedOnDateParamName, element); + baseDataValidator.reset().parameter(activatedOnDateParamName).value(activationDate).notNull(); + + throwExceptionIfValidationWarningsExist(dataValidationErrors); + } + + public void validatePreMatureAmountCalculation(final String json, final DepositAccountType depositAccountType) { + + if (StringUtils.isBlank(json)) { + throw new InvalidJsonException(); + } + + final Type typeOfMap = new TypeToken>() {}.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, + DEPOSIT_ACCOUNT_PRE_MATURE_CALCULATION_REQUEST_DATA_PARAMETERS); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(depositAccountType.resourceName()); + + final JsonElement element = this.fromApiJsonHelper.parse(json); + + final LocalDate closeDate = this.fromApiJsonHelper.extractLocalDateNamed(closedOnDateParamName, element); + baseDataValidator.reset().parameter(closedOnDateParamName).value(closeDate).notNull(); + + throwExceptionIfValidationWarningsExist(dataValidationErrors); + } + + public void validateClosing(final JsonCommand command, DepositAccountType depositAccountType, final boolean isPreMatureClose) { + final String json = command.json(); + + if (StringUtils.isBlank(json)) { + throw new InvalidJsonException(); + } + + final Type typeOfMap = new TypeToken>() {}.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, DEPOSIT_ACCOUNT_CLOSE_REQUEST_DATA_PARAMETERS); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(depositAccountType.resourceName()); + + final JsonElement element = command.parsedJson(); + + final LocalDate activationDate = this.fromApiJsonHelper.extractLocalDateNamed(closedOnDateParamName, element); + baseDataValidator.reset().parameter(closedOnDateParamName).value(activationDate).notNull(); + + final Integer onAccountClosureId = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(onAccountClosureIdParamName, element); + baseDataValidator.reset().parameter(onAccountClosureIdParamName).value(onAccountClosureId).notBlank() + .isOneOfTheseValues(DepositAccountOnClosureType.integerValues()); + + if (onAccountClosureId != null) { + final DepositAccountOnClosureType accountOnClosureType = DepositAccountOnClosureType.fromInt(onAccountClosureId); + if (accountOnClosureType.isTransferToSavings()) { + final Long toSavingsAccountId = this.fromApiJsonHelper.extractLongNamed(toSavingsAccountIdParamName, element); + baseDataValidator.reset().parameter(toSavingsAccountIdParamName).value(toSavingsAccountId) + .cantBeBlankWhenParameterProvidedIs(onAccountClosureIdParamName, + DepositAccountOnClosureType.fromInt(onAccountClosureId).getCode()); + } else if (accountOnClosureType.isReinvest() && isPreMatureClose) { + baseDataValidator.reset().parameter(onAccountClosureIdParamName).value(onAccountClosureId) + .failWithCode("reinvest.not.allowed", "Re-Invest is not supported for account pre mature close"); + } + } + + // Validate all string payment detail fields for max length + final Integer paymentTypeId = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(paymentTypeIdParamName, element); + baseDataValidator.reset().parameter(paymentTypeIdParamName).value(paymentTypeId).ignoreIfNull().integerGreaterThanZero(); + final Set paymentDetailParameters = new HashSet<>(Arrays.asList(transactionAccountNumberParamName, checkNumberParamName, + routingCodeParamName, receiptNumberParamName, bankNumberParamName)); + for (final String paymentDetailParameterName : paymentDetailParameters) { + final String paymentDetailParameterValue = this.fromApiJsonHelper.extractStringNamed(paymentDetailParameterName, element); + baseDataValidator.reset().parameter(paymentDetailParameterName).value(paymentDetailParameterValue).ignoreIfNull() + .notExceedingLengthOf(50); + } + + throwExceptionIfValidationWarningsExist(dataValidationErrors); + } + + private void throwExceptionIfValidationWarningsExist(final List dataValidationErrors) { + if (!dataValidationErrors.isEmpty()) { + // + throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", + dataValidationErrors); + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/CurrentAccountTransactionWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/CurrentAccountTransactionWritePlatformServiceImpl.java index cde77dd4d..09b0ec6d5 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/CurrentAccountTransactionWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/CurrentAccountTransactionWritePlatformServiceImpl.java @@ -43,6 +43,7 @@ import org.apache.fineract.portfolio.currentaccount.exception.CurrentAccountNotF import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountTransactionWritePlatformService; import org.apache.fineract.portfolio.currentproduct.domain.CurrentProduct; import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductRepository; +import org.apache.fineract.portfolio.denominationmaster.service.DenominationBreakdownWriter; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -60,6 +61,7 @@ public class CurrentAccountTransactionWritePlatformServiceImpl implements Curren private final CurrentProductRepository currentProductRepository; private final ProductToGLAccountMappingRepository productToGLAccountMappingRepository; private final JournalEntryRepository journalEntryRepository; + private final DenominationBreakdownWriter denominationBreakdownWriter; @Transactional @Override @@ -74,6 +76,7 @@ public class CurrentAccountTransactionWritePlatformServiceImpl implements Curren final BigDecimal amount = command.bigDecimalValueOfParameterNamed("transactionAmount"); validatePositiveAmount(amount, accountId, "deposit"); + this.denominationBreakdownWriter.validateBreakdown(command, amount, "DEPOSIT"); final String refNo = command.stringValueOfParameterNamed("accountNumber"); @@ -85,6 +88,11 @@ public class CurrentAccountTransactionWritePlatformServiceImpl implements Curren currentAccountTransactionRepository.saveAndFlush(txn); + this.denominationBreakdownWriter.save(command, txn.getId(), account.getId(), account.getAccountNo(), + account.getClient() != null ? account.getClient().getId() : null, + account.getClient() != null ? account.getClient().getCifNumber() : null, txnDate, + "DEPOSIT"); + account.setAccountBalanceDerived(newBalance); account.setTotalDepositsDerived(safeBalance(account.getTotalDepositsDerived()).add(amount)); currentAccountRepository.save(account); @@ -135,6 +143,13 @@ public class CurrentAccountTransactionWritePlatformServiceImpl implements Curren account.setTotalWithdrawalsDerived(safeBalance(account.getTotalWithdrawalsDerived()).add(amount)); currentAccountRepository.save(account); + // Save denomination breakdown for WITHDRAWAL if provided + this.denominationBreakdownWriter.validateBreakdown(command, amount, "WITHDRAWAL"); + this.denominationBreakdownWriter.save(command, txn.getId(), account.getId(), account.getAccountNo(), + account.getClient() != null ? account.getClient().getId() : null, + account.getClient() != null ? account.getClient().getCifNumber() : null, txnDate, + "WITHDRAWAL"); + // Withdrawal GL: DR SAVINGS_CONTROL (liability down), CR SAVINGS_REFERENCE (cash out) postTransactionGl(account, txn, JournalEntryType.CREDIT, JournalEntryType.DEBIT, "Current Account Withdrawal"); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java index 77808a8eb..c5190d7c6 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java @@ -1,1401 +1,1419 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.fineract.portfolio.savings.service; - -import static org.apache.fineract.portfolio.savings.DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.amountParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.chargeIdParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.dueAsOfDateParamName; - -import java.math.BigDecimal; -import java.math.MathContext; -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -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 java.util.UUID; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService; -import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; -import org.apache.fineract.infrastructure.core.api.JsonCommand; -import org.apache.fineract.infrastructure.core.data.ApiParameterError; -import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; -import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; -import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; -import org.apache.fineract.infrastructure.core.domain.ExternalId; -import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; -import org.apache.fineract.infrastructure.core.exception.PlatformServiceUnavailableException; -import org.apache.fineract.infrastructure.core.service.DateUtils; -import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; -import org.apache.fineract.organisation.holiday.domain.HolidayRepositoryWrapper; -import org.apache.fineract.organisation.monetary.domain.ApplicationCurrencyRepositoryWrapper; -import org.apache.fineract.organisation.monetary.domain.Money; -import org.apache.fineract.organisation.monetary.domain.MoneyHelper; -import org.apache.fineract.organisation.office.domain.Office; -import org.apache.fineract.organisation.staff.domain.Staff; -import org.apache.fineract.organisation.workingdays.domain.WorkingDaysRepositoryWrapper; -import org.apache.fineract.portfolio.account.PortfolioAccountType; -import org.apache.fineract.portfolio.account.data.AccountTransferDTO; -import org.apache.fineract.portfolio.account.data.PortfolioAccountData; -import org.apache.fineract.portfolio.account.domain.AccountTransferType; -import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlatformService; -import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService; -import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService; -import org.apache.fineract.portfolio.calendar.domain.Calendar; -import org.apache.fineract.portfolio.calendar.domain.CalendarEntityType; -import org.apache.fineract.portfolio.calendar.domain.CalendarFrequencyType; -import org.apache.fineract.portfolio.calendar.domain.CalendarInstance; -import org.apache.fineract.portfolio.calendar.domain.CalendarInstanceRepository; -import org.apache.fineract.portfolio.calendar.domain.CalendarType; -import org.apache.fineract.portfolio.calendar.service.CalendarUtils; -import org.apache.fineract.portfolio.charge.domain.Charge; -import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper; -import org.apache.fineract.portfolio.client.domain.Client; -import org.apache.fineract.portfolio.client.exception.ClientNotActiveException; -import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; -import org.apache.fineract.portfolio.group.domain.Group; -import org.apache.fineract.portfolio.group.exception.GroupNotActiveException; -import org.apache.fineract.portfolio.note.domain.Note; -import org.apache.fineract.portfolio.note.domain.NoteRepository; -import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail; -import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService; -import org.apache.fineract.portfolio.savings.DepositAccountType; -import org.apache.fineract.portfolio.savings.DepositsApiConstants; -import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType; -import org.apache.fineract.portfolio.savings.SavingsApiConstants; -import org.apache.fineract.portfolio.savings.data.DepositAccountTransactionDataValidator; -import org.apache.fineract.portfolio.savings.data.SavingsAccountChargeDataValidator; -import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionDTO; -import org.apache.fineract.portfolio.savings.domain.DepositAccountAssembler; -import org.apache.fineract.portfolio.savings.domain.DepositAccountDomainService; -import org.apache.fineract.portfolio.savings.domain.DepositAccountOnHoldTransaction; -import org.apache.fineract.portfolio.savings.domain.DepositAccountOnHoldTransactionRepository; -import org.apache.fineract.portfolio.savings.domain.DepositAccountRecurringDetail; -import org.apache.fineract.portfolio.savings.domain.FixedDepositAccount; -import org.apache.fineract.portfolio.savings.domain.RecurringDepositAccount; -import org.apache.fineract.portfolio.savings.domain.SavingsAccount; -import org.apache.fineract.portfolio.savings.domain.SavingsAccountCharge; -import org.apache.fineract.portfolio.savings.domain.SavingsAccountChargeRepositoryWrapper; -import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper; -import org.apache.fineract.portfolio.savings.domain.SavingsAccountStatusType; -import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction; -import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransactionRepository; -import org.apache.fineract.portfolio.savings.exception.DepositAccountTransactionNotAllowedException; -import org.apache.fineract.portfolio.savings.exception.SavingsAccountTransactionNotFoundException; -import org.apache.fineract.portfolio.savings.exception.TransactionUpdateNotAllowedException; -import org.apache.fineract.useradministration.domain.AppUser; -import org.springframework.transaction.annotation.Transactional; - -@Slf4j -@Transactional -@RequiredArgsConstructor -public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements DepositAccountWritePlatformService { - - private final PlatformSecurityContext context; - private final SavingsAccountRepositoryWrapper savingAccountRepositoryWrapper; - private final SavingsAccountTransactionRepository savingsAccountTransactionRepository; - private final DepositAccountAssembler depositAccountAssembler; - private final DepositAccountTransactionDataValidator depositAccountTransactionDataValidator; - private final SavingsAccountChargeDataValidator savingsAccountChargeDataValidator; - private final PaymentDetailWritePlatformService paymentDetailWritePlatformService; - private final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepositoryWrapper; - private final JournalEntryWritePlatformService journalEntryWritePlatformService; - private final DepositAccountDomainService depositAccountDomainService; - private final NoteRepository noteRepository; - private final AccountTransfersReadPlatformService accountTransfersReadPlatformService; - private final ChargeRepositoryWrapper chargeRepository; - private final SavingsAccountChargeRepositoryWrapper savingsAccountChargeRepository; - private final AccountAssociationsReadPlatformService accountAssociationsReadPlatformService; - private final AccountTransfersWritePlatformService accountTransfersWritePlatformService; - private final DepositAccountReadPlatformService depositAccountReadPlatformService; - private final CalendarInstanceRepository calendarInstanceRepository; - private final ConfigurationDomainService configurationDomainService; - private final HolidayRepositoryWrapper holidayRepository; - private final WorkingDaysRepositoryWrapper workingDaysRepository; - private final DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository; - - @Transactional - @Override - public CommandProcessingResult activateFDAccount(final Long savingsId, final JsonCommand command) { - final AppUser user = context.authenticatedUser(); - - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - final boolean postReversals = false; - this.depositAccountTransactionDataValidator.validateActivation(command); - final MathContext mc = MathContext.DECIMAL64; - final FixedDepositAccount account = (FixedDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId, - DepositAccountType.FIXED_DEPOSIT); - checkClientOrGroupActive(account); - - final Set existingTransactionIds = new HashSet<>(); - final Set existingReversedTransactionIds = new HashSet<>(); - updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); - - final Map changes = account.activate(user, command); - Money activationChargeAmount = getActivationCharge(account); - if (!changes.isEmpty()) { - final Locale locale = command.extractLocale(); - final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); - Money amountForDeposit = account.activateWithBalance().plus(activationChargeAmount); - if (amountForDeposit.isGreaterThanZero()) { - final PortfolioAccountData portfolioAccountData = this.accountAssociationsReadPlatformService - .retriveSavingsLinkedAssociation(savingsId); - - if (portfolioAccountData == null) { - final PaymentDetail paymentDetail = null; - this.depositAccountDomainService.handleFDDeposit(account, fmt, account.getActivationDate(), - amountForDeposit.getAmount(), paymentDetail); - } else { - final SavingsAccount fromSavingsAccount = null; - boolean isRegularTransaction = false; - final boolean isExceptionForBalanceCheck = false; - final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(account.getActivationDate(), - amountForDeposit.getAmount(), PortfolioAccountType.SAVINGS, PortfolioAccountType.SAVINGS, - portfolioAccountData.getId(), account.getId(), "Account Transfer", locale, fmt, null, null, null, null, null, - AccountTransferType.ACCOUNT_TRANSFER.getValue(), null, null, ExternalId.empty(), null, account, - fromSavingsAccount, isRegularTransaction, isExceptionForBalanceCheck); - this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO); - } - final boolean isInterestTransfer = false; - final LocalDate postInterestOnDate = null; - if (activationChargeAmount.isGreaterThanZero()) { - payActivationCharge(account); - } - if (account.isBeforeLastPostingPeriod(account.getActivationDate(), false)) { - final LocalDate today = DateUtils.getBusinessLocalDate(); - account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate, false); - } else { - final LocalDate today = DateUtils.getBusinessLocalDate(); - account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate, false, postReversals); - } - } - - final boolean isPreMatureClosure = false; - account.updateMaturityDateAndAmount(mc, isPreMatureClosure, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth); - List depositAccountOnHoldTransactions = null; - if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { - depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository - .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); - } - account.validateAccountBalanceDoesNotBecomeNegative(SavingsAccountTransactionType.PAY_CHARGE.name(), - depositAccountOnHoldTransactions, false); - this.savingAccountRepositoryWrapper.saveAndFlush(account); - } - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds); - - return new CommandProcessingResultBuilder() // - .withEntityId(savingsId) // - .withOfficeId(account.officeId()) // - .withClientId(account.clientId()) // - .withGroupId(account.groupId()) // - .withSavingsId(savingsId) // - .with(changes) // - .build(); - } - - private Money getActivationCharge(final FixedDepositAccount account) { - Money activationChargeAmount = Money.zero(account.getCurrency()); - for (SavingsAccountCharge savingsAccountCharge : account.charges()) { - if (savingsAccountCharge.isSavingsActivation()) { - activationChargeAmount = activationChargeAmount.plus(savingsAccountCharge.getAmount(account.getCurrency())); - } - } - return activationChargeAmount; - } - - private void payActivationCharge(final FixedDepositAccount account) { - for (SavingsAccountCharge savingsAccountCharge : account.charges()) { - if (savingsAccountCharge.isSavingsActivation()) { - account.payCharge(savingsAccountCharge, savingsAccountCharge.getAmount(account.getCurrency()), account.getActivationDate(), - false, null); - } - } - } - - @Transactional - @Override - public CommandProcessingResult activateRDAccount(final Long savingsId, final JsonCommand command) { - boolean isRegularTransaction = false; - - final AppUser user = this.context.authenticatedUser(); - - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - final boolean postReversals = false; - this.depositAccountTransactionDataValidator.validateActivation(command); - - final RecurringDepositAccount account = (RecurringDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId, - DepositAccountType.RECURRING_DEPOSIT); - checkClientOrGroupActive(account); - - final Set existingTransactionIds = new HashSet<>(); - final Set existingReversedTransactionIds = new HashSet<>(); - updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); - - final Map changes = account.activate(user, command); - - if (!changes.isEmpty()) { - final Locale locale = command.extractLocale(); - final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); - Money amountForDeposit = account.activateWithBalance(); - if (amountForDeposit.isGreaterThanZero()) { - final PortfolioAccountData portfolioAccountData = this.accountAssociationsReadPlatformService - .retriveSavingsLinkedAssociation(savingsId); - if (portfolioAccountData == null) { - this.depositAccountDomainService.handleRDDeposit(account, fmt, account.getActivationDate(), - amountForDeposit.getAmount(), null, isRegularTransaction); - } else { - final boolean isExceptionForBalanceCheck = false; - final SavingsAccount fromSavingsAccount = null; - final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(account.getActivationDate(), - amountForDeposit.getAmount(), PortfolioAccountType.SAVINGS, PortfolioAccountType.SAVINGS, - portfolioAccountData.getId(), account.getId(), "Account Transfer", locale, fmt, null, null, null, null, null, - AccountTransferType.ACCOUNT_TRANSFER.getValue(), null, null, ExternalId.empty(), null, account, - fromSavingsAccount, isRegularTransaction, isExceptionForBalanceCheck); - this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO); - } - } - - final MathContext mc = MathContext.DECIMAL64; - - // submitted and activation date are different then recalculate - // maturity date and schedule - if (!account.accountSubmittedAndActivationOnSameDate()) { - final boolean isPreMatureClosure = false; - final CalendarInstance calendarInstance = this.calendarInstanceRepository.findByEntityIdAndEntityTypeIdAndCalendarTypeId( - savingsId, CalendarEntityType.SAVINGS.getValue(), CalendarType.COLLECTION.getValue()); - - final Calendar calendar = calendarInstance.getCalendar(); - final PeriodFrequencyType frequencyType = CalendarFrequencyType.from(CalendarUtils.getFrequency(calendar.getRecurrence())); - Integer frequency = CalendarUtils.getInterval(calendar.getRecurrence()); - frequency = frequency == -1 ? 1 : frequency; - account.generateSchedule(frequencyType, frequency, calendar); - account.updateMaturityDateAndAmount(mc, isPreMatureClosure, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth); - } - - final LocalDate overdueUptoDate = DateUtils.getBusinessLocalDate(); - account.updateOverduePayments(overdueUptoDate); - final boolean isInterestTransfer = false; - final LocalDate postInterestOnDate = null; - if (account.isBeforeLastPostingPeriod(account.getActivationDate(), false)) { - final LocalDate today = DateUtils.getBusinessLocalDate(); - account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, - postInterestOnDate, false, postReversals); - } else { - final LocalDate today = DateUtils.getBusinessLocalDate(); - account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate, false, postReversals); - } - List depositAccountOnHoldTransactions = null; - if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { - depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository - .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); - } - - account.validateAccountBalanceDoesNotBecomeNegative(SavingsAccountTransactionType.PAY_CHARGE.name(), - depositAccountOnHoldTransactions, false); - - this.savingAccountRepositoryWrapper.saveAndFlush(account); - } - - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds); - - return new CommandProcessingResultBuilder() // - .withEntityId(savingsId) // - .withOfficeId(account.officeId()) // - .withClientId(account.clientId()) // - .withGroupId(account.groupId()) // - .withSavingsId(savingsId) // - .with(changes) // - .build(); - } - - @Transactional - @Override - public CommandProcessingResult depositToFDAccount(final Long savingsId, @SuppressWarnings("unused") final JsonCommand command) { - // this.context.authenticatedUser(); - this.depositAccountTransactionDataValidator.validate(command, DepositAccountType.FIXED_DEPOSIT); - - final FixedDepositAccount account = (FixedDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId, - DepositAccountType.FIXED_DEPOSIT); - checkClientOrGroupActive(account); - - final Locale locale = command.extractLocale(); - final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); - - final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); - final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount"); - - final Map changes = new LinkedHashMap<>(); - final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); - final SavingsAccountTransaction deposit = this.depositAccountDomainService.handleFDDeposit(account, fmt, transactionDate, - transactionAmount, paymentDetail); - - return new CommandProcessingResultBuilder().withEntityId(deposit.getId()).withOfficeId(account.officeId()) - .withClientId(account.clientId()).withGroupId(account.groupId()).withSavingsId(savingsId).with(changes).build(); - - } - - @Transactional - @Override - public CommandProcessingResult updateDepositAmountForRDAccount(Long savingsId, JsonCommand command) { - this.depositAccountTransactionDataValidator.validateDepositAmountUpdate(command); - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - - final BigDecimal mandatoryRecommendedDepositAmount = command - .bigDecimalValueOfParameterNamed(DepositsApiConstants.mandatoryRecommendedDepositAmountParamName); - - final LocalDate depositAmountUpdateEffectiveFromDate = command - .localDateValueOfParameterNamed(DepositsApiConstants.effectiveDateParamName); - - final RecurringDepositAccount recurringDepositAccount = (RecurringDepositAccount) this.depositAccountAssembler - .assembleFrom(savingsId, DepositAccountType.RECURRING_DEPOSIT); - DepositAccountRecurringDetail recurringDetail = recurringDepositAccount.getRecurringDetail(); - Map changes = recurringDetail.updateMandatoryRecommendedDepositAmount(mandatoryRecommendedDepositAmount, - depositAmountUpdateEffectiveFromDate, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth); - - return new CommandProcessingResultBuilder() // - .withEntityId(savingsId) // - .withOfficeId(recurringDepositAccount.officeId()) // - .withClientId(recurringDepositAccount.clientId()) // - .withGroupId(recurringDepositAccount.groupId()) // - .withSavingsId(savingsId) // - .with(changes) // - .build(); - } - - @Transactional - @Override - public CommandProcessingResult depositToRDAccount(final Long savingsId, final JsonCommand command) { - boolean isRegularTransaction = true; - - this.depositAccountTransactionDataValidator.validate(command, DepositAccountType.RECURRING_DEPOSIT); - - final RecurringDepositAccount account = (RecurringDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId, - DepositAccountType.RECURRING_DEPOSIT); - checkClientOrGroupActive(account); - - final Locale locale = command.extractLocale(); - final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); - - final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); - final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount"); - - final Map changes = new LinkedHashMap<>(); - final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); - final SavingsAccountTransaction deposit = this.depositAccountDomainService.handleRDDeposit(account, fmt, transactionDate, - transactionAmount, paymentDetail, isRegularTransaction); - - return new CommandProcessingResultBuilder() // - .withEntityId(deposit.getId()) // - .withOfficeId(account.officeId()) // - .withClientId(account.clientId()) // - .withGroupId(account.groupId()) // - .withSavingsId(savingsId) // - .with(changes) // - .build(); - } - - private Long saveTransactionToGenerateTransactionId(final SavingsAccountTransaction transaction) { - this.savingsAccountTransactionRepository.saveAndFlush(transaction); - return transaction.getId(); - } - - @Transactional - @Override - public CommandProcessingResult withdrawal(final Long savingsId, final JsonCommand command, - final DepositAccountType depositAccountType) { - - boolean isRegularTransaction = true; - - this.depositAccountTransactionDataValidator.validate(command, depositAccountType); - - final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); - final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount"); - - final Locale locale = command.extractLocale(); - final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); - - final Map changes = new LinkedHashMap<>(); - final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); - - final SavingsAccount account = this.depositAccountAssembler.assembleFrom(savingsId, depositAccountType); - - checkClientOrGroupActive(account); - - final SavingsAccountTransaction withdrawal = this.depositAccountDomainService.handleWithdrawal(account, fmt, transactionDate, - transactionAmount, paymentDetail, true, isRegularTransaction); - - return new CommandProcessingResultBuilder() // - .withEntityId(withdrawal.getId()) // - .withOfficeId(account.officeId()) // - .withClientId(account.clientId()) // - .withGroupId(account.groupId()) // - .withSavingsId(savingsId) // - .with(changes)// - .build(); - } - - @Transactional - @Override - public CommandProcessingResult calculateInterest(final Long savingsId, final DepositAccountType depositAccountType) { - - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - - final SavingsAccount account = this.depositAccountAssembler.assembleFrom(savingsId, depositAccountType); - checkClientOrGroupActive(account); - - final LocalDate today = DateUtils.getBusinessLocalDate(); - final boolean postReversals = false; - final MathContext mc = new MathContext(15, MoneyHelper.getRoundingMode()); - boolean isInterestTransfer = false; - LocalDate postInterestOnDate = null; - account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate, false, postReversals); - - this.savingAccountRepositoryWrapper.save(account); - - return new CommandProcessingResultBuilder() // - .withEntityId(savingsId) // - .withOfficeId(account.officeId()) // - .withClientId(account.clientId()) // - .withGroupId(account.groupId()) // - .withSavingsId(savingsId) // - .build(); - } - - @Transactional - @Override - public CommandProcessingResult postInterest(final Long savingsId, final DepositAccountType depositAccountType) { - - final SavingsAccount account = this.depositAccountAssembler.assembleFrom(savingsId, depositAccountType); - checkClientOrGroupActive(account); - postInterest(account); - return new CommandProcessingResultBuilder() // - .withEntityId(savingsId) // - .withOfficeId(account.officeId()) // - .withClientId(account.clientId()) // - .withGroupId(account.groupId()) // - .withSavingsId(savingsId) // - .build(); - } - - @Transactional - private void postInterest(final SavingsAccount account) { - - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - - final Set existingTransactionIds = new HashSet<>(); - final Set existingReversedTransactionIds = new HashSet<>(); - updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); - final LocalDate today = DateUtils.getBusinessLocalDate(); - final MathContext mc = new MathContext(10, MoneyHelper.getRoundingMode()); - boolean isInterestTransfer = false; - LocalDate postInterestOnDate = null; - final boolean postReversals = false; - account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, - postInterestOnDate, false, postReversals); - this.savingAccountRepositoryWrapper.saveAndFlush(account); - - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds); - } - - @Override - public CommandProcessingResult undoFDTransaction(final Long savingsId, @SuppressWarnings("unused") final Long transactionId, - @SuppressWarnings("unused") final boolean allowAccountTransferModification) { - - throw new DepositAccountTransactionNotAllowedException(savingsId, "undo", DepositAccountType.FIXED_DEPOSIT); - } - - @Override - public CommandProcessingResult undoRDTransaction(final Long savingsId, final Long transactionId, - final boolean allowAccountTransferModification) { - - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - - final RecurringDepositAccount account = (RecurringDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId, - DepositAccountType.RECURRING_DEPOSIT); - final Set existingTransactionIds = new HashSet<>(); - final Set existingReversedTransactionIds = new HashSet<>(); - updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); - - final SavingsAccountTransaction savingsAccountTransaction = this.savingsAccountTransactionRepository - .findOneByIdAndSavingsAccountId(transactionId, savingsId); - if (savingsAccountTransaction == null) { - throw new SavingsAccountTransactionNotFoundException(savingsId, transactionId); - } - - if (!allowAccountTransferModification - && this.accountTransfersReadPlatformService.isAccountTransfer(transactionId, PortfolioAccountType.SAVINGS)) { - throw new PlatformServiceUnavailableException("error.msg.recurring.deposit.account.transfer.transaction.update.not.allowed", - "Recurring deposit account transaction:" + transactionId + " update not allowed as it involves in account transfer", - transactionId); - } - - final LocalDate today = DateUtils.getBusinessLocalDate(); - final MathContext mc = MathContext.DECIMAL64; - - if (account.isNotActive()) { - throwValidationForActiveStatus(SavingsApiConstants.undoTransactionAction); - } - account.undoTransaction(transactionId); - boolean isInterestTransfer = false; - LocalDate postInterestOnDate = null; - checkClientOrGroupActive(account); - final boolean postReversals = false; - if (savingsAccountTransaction.isPostInterestCalculationRequired() - && account.isBeforeLastPostingPeriod(savingsAccountTransaction.getTransactionDate(), false)) { - account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, - postInterestOnDate, false, postReversals); - } else { - account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate, false, postReversals); - } - List depositAccountOnHoldTransactions = null; - if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { - depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository - .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); - } - - account.validateAccountBalanceDoesNotBecomeNegative(SavingsApiConstants.undoTransactionAction, depositAccountOnHoldTransactions, - false); - // account.activateAccountBasedOnBalance(); - final boolean isPreMatureClosure = false; - account.updateMaturityDateAndAmount(mc, isPreMatureClosure, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth); - - final LocalDate overdueUptoDate = DateUtils.getBusinessLocalDate(); - - if (savingsAccountTransaction.isDeposit()) { - account.updateScheduleInstallments(); - } - - account.updateOverduePayments(overdueUptoDate); - - this.savingAccountRepositoryWrapper.saveAndFlush(account); - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds); - - return new CommandProcessingResultBuilder() // - .withEntityId(savingsId) // - .withOfficeId(account.officeId()) // - .withClientId(account.clientId()) // - .withGroupId(account.groupId()) // - .withSavingsId(savingsId) // - .build(); - } - - @Override - public CommandProcessingResult adjustFDTransaction(final Long savingsId, @SuppressWarnings("unused") final Long transactionId, - @SuppressWarnings("unused") final JsonCommand command) { - - throw new DepositAccountTransactionNotAllowedException(savingsId, "modify", DepositAccountType.FIXED_DEPOSIT); - } - - @Override - public CommandProcessingResult adjustRDTransaction(final Long savingsId, final Long transactionId, final JsonCommand command) { - context.authenticatedUser(); - - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - final Long relaxingDaysConfigForPivotDate = this.configurationDomainService.retrieveRelaxingDaysConfigForPivotDate(); - this.depositAccountTransactionDataValidator.validate(command, DepositAccountType.RECURRING_DEPOSIT); - - final SavingsAccountTransaction savingsAccountTransaction = this.savingsAccountTransactionRepository - .findOneByIdAndSavingsAccountId(transactionId, savingsId); - if (savingsAccountTransaction == null) { - throw new SavingsAccountTransactionNotFoundException(savingsId, transactionId); - } - - if ((!savingsAccountTransaction.isDeposit() && !savingsAccountTransaction.isWithdrawal()) - || savingsAccountTransaction.isReversed()) { - throw new TransactionUpdateNotAllowedException(savingsId, transactionId); - } - - if (this.accountTransfersReadPlatformService.isAccountTransfer(transactionId, PortfolioAccountType.SAVINGS)) { - throw new PlatformServiceUnavailableException("error.msg.saving.account.transfer.transaction.update.not.allowed", - "Deposit account transaction:" + transactionId + " update not allowed as it involves in account transfer", - transactionId); - } - - final LocalDate today = DateUtils.getBusinessLocalDate(); - - final RecurringDepositAccount account = (RecurringDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId, - DepositAccountType.RECURRING_DEPOSIT); - if (account.isNotActive()) { - throwValidationForActiveStatus(SavingsApiConstants.adjustTransactionAction); - } - final Set existingTransactionIds = new HashSet<>(); - final Set existingReversedTransactionIds = new HashSet<>(); - updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); - - final Locale locale = command.extractLocale(); - final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); - final LocalDate transactionDate = command.localDateValueOfParameterNamed(SavingsApiConstants.transactionDateParamName); - final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed(SavingsApiConstants.transactionAmountParamName); - final Map changes = new LinkedHashMap<>(); - final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); - - final MathContext mc = new MathContext(10, MoneyHelper.getRoundingMode()); - account.undoTransaction(transactionId); - - SavingsAccountTransaction transaction = null; - Integer accountType = null; - UUID refNo = UUID.randomUUID(); - if (savingsAccountTransaction.isDeposit()) { - final SavingsAccountTransactionDTO transactionDTO = new SavingsAccountTransactionDTO(fmt, transactionDate, transactionAmount, - paymentDetail, null, accountType); - transaction = account.deposit(transactionDTO, false, relaxingDaysConfigForPivotDate, refNo.toString()); - } else { - final SavingsAccountTransactionDTO transactionDTO = new SavingsAccountTransactionDTO(fmt, transactionDate, transactionAmount, - paymentDetail, null, accountType); - transaction = account.withdraw(transactionDTO, true, false, relaxingDaysConfigForPivotDate, refNo.toString()); - } - final Long newtransactionId = saveTransactionToGenerateTransactionId(transaction); - boolean isInterestTransfer = false; - final LocalDate postInterestOnDate = null; - final boolean postReversals = false; - if (account.isBeforeLastPostingPeriod(transactionDate, false) - || account.isBeforeLastPostingPeriod(savingsAccountTransaction.getTransactionDate(), false)) { - account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, - postInterestOnDate, false, postReversals); - } else { - account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate, false, postReversals); - } - List depositAccountOnHoldTransactions = null; - if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { - depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository - .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); - } - - account.validateAccountBalanceDoesNotBecomeNegative(SavingsApiConstants.adjustTransactionAction, depositAccountOnHoldTransactions, - false); - account.activateAccountBasedOnBalance(); - - if (savingsAccountTransaction.isDeposit()) { - account.handleScheduleInstallments(savingsAccountTransaction); - } - final LocalDate overdueUptoDate = DateUtils.getBusinessLocalDate(); - account.updateOverduePayments(overdueUptoDate); - - this.savingAccountRepositoryWrapper.saveAndFlush(account); - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds); - return new CommandProcessingResultBuilder() // - .withEntityId(newtransactionId) // - .withOfficeId(account.officeId()) // - .withClientId(account.clientId()) // - .withGroupId(account.groupId()) // - .withSavingsId(savingsId) // - .with(changes)// - .build(); - } - - /** - * - */ - private void throwValidationForActiveStatus(final String actionName) { - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(SAVINGS_ACCOUNT_RESOURCE_NAME + actionName); - baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("account.is.not.active"); - throw new PlatformApiDataValidationException(dataValidationErrors); - } - - private void checkClientOrGroupActive(final SavingsAccount account) { - final Client client = account.getClient(); - if (client != null && client.isNotActive()) { - throw new ClientNotActiveException(client.getId()); - } - final Group group = account.group(); - if (group != null && group.isNotActive()) { - throw new GroupNotActiveException(group.getId()); - } - } - - @Override - public CommandProcessingResult closeFDAccount(final Long savingsId, final JsonCommand command) { - final AppUser user = this.context.authenticatedUser(); - final boolean isPreMatureClose = false; - this.depositAccountTransactionDataValidator.validateClosing(command, DepositAccountType.FIXED_DEPOSIT, isPreMatureClose); - - final Map changes = new LinkedHashMap<>(); - final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); - - final FixedDepositAccount account = (FixedDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId, - DepositAccountType.FIXED_DEPOSIT); - checkClientOrGroupActive(account); - - this.depositAccountDomainService.handleFDAccountClosure(account, paymentDetail, user, command, changes); - - final String noteText = command.stringValueOfParameterNamed("note"); - if (StringUtils.isNotBlank(noteText)) { - final Note note = Note.savingNote(account, noteText); - changes.put("note", noteText); - this.noteRepository.save(note); - } - - return new CommandProcessingResultBuilder() // - .withEntityId(savingsId) // - .withOfficeId(account.officeId()) // - .withClientId(account.clientId()) // - .withGroupId(account.groupId()) // - .withSavingsId(savingsId) // - .with(changes)// - .build(); - - } - - @Override - public CommandProcessingResult closeRDAccount(final Long savingsId, final JsonCommand command) { - final AppUser user = this.context.authenticatedUser(); - - this.depositAccountTransactionDataValidator.validateClosing(command, DepositAccountType.RECURRING_DEPOSIT, false); - - final Map changes = new LinkedHashMap<>(); - final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); - - final RecurringDepositAccount account = (RecurringDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId, - DepositAccountType.RECURRING_DEPOSIT); - checkClientOrGroupActive(account); - - this.depositAccountDomainService.handleRDAccountClosure(account, paymentDetail, user, command, changes); - - final String noteText = command.stringValueOfParameterNamed("note"); - if (StringUtils.isNotBlank(noteText)) { - final Note note = Note.savingNote(account, noteText); - changes.put("note", noteText); - this.noteRepository.save(note); - } - - return new CommandProcessingResultBuilder() // - .withEntityId(savingsId) // - .withOfficeId(account.officeId()) // - .withClientId(account.clientId()) // - .withGroupId(account.groupId()) // - .withSavingsId(savingsId) // - .with(changes)// - .build(); - - } - - @Override - public CommandProcessingResult prematureCloseFDAccount(final Long savingsId, final JsonCommand command) { - final AppUser user = this.context.authenticatedUser(); - - this.depositAccountTransactionDataValidator.validateClosing(command, DepositAccountType.FIXED_DEPOSIT, true); - - final Map changes = new LinkedHashMap<>(); - final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); - - final FixedDepositAccount account = (FixedDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId, - DepositAccountType.FIXED_DEPOSIT); - checkClientOrGroupActive(account); - - this.depositAccountDomainService.handleFDAccountPreMatureClosure(account, paymentDetail, user, command, changes); - - final String noteText = command.stringValueOfParameterNamed("note"); - if (StringUtils.isNotBlank(noteText)) { - final Note note = Note.savingNote(account, noteText); - changes.put("note", noteText); - this.noteRepository.save(note); - } - - return new CommandProcessingResultBuilder() // - .withEntityId(savingsId) // - .withOfficeId(account.officeId()) // - .withClientId(account.clientId()) // - .withGroupId(account.groupId()) // - .withSavingsId(savingsId) // - .with(changes)// - .build(); - - } - - @Override - public CommandProcessingResult prematureCloseRDAccount(final Long savingsId, final JsonCommand command) { - final AppUser user = this.context.authenticatedUser(); - - this.depositAccountTransactionDataValidator.validateClosing(command, DepositAccountType.RECURRING_DEPOSIT, true); - - final Map changes = new LinkedHashMap<>(); - final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); - - final RecurringDepositAccount account = (RecurringDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId, - DepositAccountType.RECURRING_DEPOSIT); - checkClientOrGroupActive(account); - if (account.maturityDate() == null) { - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME + DepositsApiConstants.preMatureCloseAction); - baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("can.not.close.as.premature"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - this.depositAccountDomainService.handleRDAccountPreMatureClosure(account, paymentDetail, user, command, changes); - - final String noteText = command.stringValueOfParameterNamed("note"); - if (StringUtils.isNotBlank(noteText)) { - final Note note = Note.savingNote(account, noteText); - changes.put("note", noteText); - this.noteRepository.save(note); - } - - return new CommandProcessingResultBuilder() // - .withEntityId(savingsId) // - .withOfficeId(account.officeId()) // - .withClientId(account.clientId()) // - .withGroupId(account.groupId()) // - .withSavingsId(savingsId) // - .with(changes)// - .build(); - - } - - @Override - public SavingsAccountTransaction initiateSavingsTransfer(final Long accountId, final LocalDate transferDate, - final DepositAccountType depositAccountType) { - context.authenticatedUser(); - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - - final SavingsAccount savingsAccount = this.depositAccountAssembler.assembleFrom(accountId, depositAccountType); - final LocalDate postInterestOnDate = null; - final Set existingTransactionIds = new HashSet<>(); - final Set existingReversedTransactionIds = new HashSet<>(); - updateExistingTransactionsDetails(savingsAccount, existingTransactionIds, existingReversedTransactionIds); - - final SavingsAccountTransaction newTransferTransaction = SavingsAccountTransaction.initiateTransfer(savingsAccount, - savingsAccount.office(), transferDate); - savingsAccount.addTransaction(newTransferTransaction); - savingsAccount.setStatus(SavingsAccountStatusType.TRANSFER_IN_PROGRESS.getValue()); - final MathContext mc = MathContext.DECIMAL64; - boolean isInterestTransfer = false; - final boolean postReversals = false; - savingsAccount.calculateInterestUsing(mc, transferDate, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate, false, postReversals); - - this.savingsAccountTransactionRepository.save(newTransferTransaction); - this.savingAccountRepositoryWrapper.saveAndFlush(savingsAccount); - - postJournalEntries(savingsAccount, existingTransactionIds, existingReversedTransactionIds); - - return newTransferTransaction; - } - - @Override - public SavingsAccountTransaction withdrawSavingsTransfer(final Long accountId, final LocalDate transferDate, - final DepositAccountType depositAccountType) { - context.authenticatedUser(); - - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - - final SavingsAccount savingsAccount = this.depositAccountAssembler.assembleFrom(accountId, depositAccountType); - - final Set existingTransactionIds = new HashSet<>(); - final Set existingReversedTransactionIds = new HashSet<>(); - updateExistingTransactionsDetails(savingsAccount, existingTransactionIds, existingReversedTransactionIds); - - final SavingsAccountTransaction withdrawtransferTransaction = SavingsAccountTransaction.withdrawTransfer(savingsAccount, - savingsAccount.office(), transferDate); - savingsAccount.addTransaction(withdrawtransferTransaction); - savingsAccount.setStatus(SavingsAccountStatusType.ACTIVE.getValue()); - final boolean postReversals = false; - final MathContext mc = MathContext.DECIMAL64; - boolean isInterestTransfer = false; - LocalDate postInterestOnDate = null; - savingsAccount.calculateInterestUsing(mc, transferDate, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate, false, postReversals); - - this.savingsAccountTransactionRepository.save(withdrawtransferTransaction); - this.savingAccountRepositoryWrapper.saveAndFlush(savingsAccount); - - postJournalEntries(savingsAccount, existingTransactionIds, existingReversedTransactionIds); - - return withdrawtransferTransaction; - } - - @Override - public void rejectSavingsTransfer(final Long accountId, final DepositAccountType depositAccountType) { - final SavingsAccount savingsAccount = this.depositAccountAssembler.assembleFrom(accountId, depositAccountType); - savingsAccount.setStatus(SavingsAccountStatusType.TRANSFER_ON_HOLD.getValue()); - this.savingAccountRepositoryWrapper.save(savingsAccount); - } - - @Override - public SavingsAccountTransaction acceptSavingsTransfer(final Long accountId, final LocalDate transferDate, - final Office acceptedInOffice, final Staff fieldOfficer, final DepositAccountType depositAccountType) { - context.authenticatedUser(); - - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - - final SavingsAccount savingsAccount = this.depositAccountAssembler.assembleFrom(accountId, depositAccountType); - - final Set existingTransactionIds = new HashSet<>(); - final Set existingReversedTransactionIds = new HashSet<>(); - updateExistingTransactionsDetails(savingsAccount, existingTransactionIds, existingReversedTransactionIds); - - final SavingsAccountTransaction acceptTransferTransaction = SavingsAccountTransaction.approveTransfer(savingsAccount, - acceptedInOffice, transferDate); - savingsAccount.addTransaction(acceptTransferTransaction); - savingsAccount.setStatus(SavingsAccountStatusType.ACTIVE.getValue()); - if (fieldOfficer != null) { - savingsAccount.reassignSavingsOfficer(fieldOfficer, transferDate); - } - boolean isInterestTransfer = false; - final boolean postReversals = false; - LocalDate postInterestOnDate = null; - final MathContext mc = MathContext.DECIMAL64; - savingsAccount.calculateInterestUsing(mc, transferDate, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate, false, postReversals); - - this.savingsAccountTransactionRepository.save(acceptTransferTransaction); - this.savingAccountRepositoryWrapper.saveAndFlush(savingsAccount); - - postJournalEntries(savingsAccount, existingTransactionIds, existingReversedTransactionIds); - - return acceptTransferTransaction; - } - - @Transactional - @Override - public CommandProcessingResult addSavingsAccountCharge(final JsonCommand command, final DepositAccountType depositAccountType) { - - this.context.authenticatedUser(); - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(SAVINGS_ACCOUNT_RESOURCE_NAME); - - final Long savingsAccountId = command.getSavingsId(); - this.savingsAccountChargeDataValidator.validateAdd(command.json()); - - final SavingsAccount savingsAccount = this.depositAccountAssembler.assembleFrom(savingsAccountId, depositAccountType); - checkClientOrGroupActive(savingsAccount); - - final Locale locale = command.extractLocale(); - final String format = command.dateFormat(); - final DateTimeFormatter fmt = StringUtils.isNotBlank(format) ? DateTimeFormatter.ofPattern(format).withLocale(locale) - : DateTimeFormatter.ofPattern("dd MM yyyy"); - - final Long chargeDefinitionId = command.longValueOfParameterNamed(chargeIdParamName); - final Charge chargeDefinition = this.chargeRepository.findOneWithNotFoundDetection(chargeDefinitionId); - - final SavingsAccountCharge savingsAccountCharge = SavingsAccountCharge.createNewFromJson(savingsAccount, chargeDefinition, command); - - if (savingsAccountCharge.getDueDate() != null) { - // transaction date should not be on a holiday or non working day - if (!this.configurationDomainService.allowTransactionsOnHolidayEnabled() - && this.holidayRepository.isHoliday(savingsAccount.officeId(), savingsAccountCharge.getDueDate())) { - baseDataValidator.reset().parameter(dueAsOfDateParamName).value(savingsAccountCharge.getDueDate().format(fmt)) - .failWithCodeNoParameterAddedToErrorCode("charge.due.date.is.on.holiday"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - if (!this.configurationDomainService.allowTransactionsOnNonWorkingDayEnabled() - && !this.workingDaysRepository.isWorkingDay(savingsAccountCharge.getDueDate())) { - baseDataValidator.reset().parameter(dueAsOfDateParamName).value(savingsAccountCharge.getDueDate().format(fmt)) - .failWithCodeNoParameterAddedToErrorCode("charge.due.date.is.a.nonworking.day"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - } - - savingsAccount.addCharge(fmt, savingsAccountCharge, chargeDefinition); - - this.savingAccountRepositoryWrapper.saveAndFlush(savingsAccount); - - return new CommandProcessingResultBuilder() // - .withEntityId(savingsAccountCharge.getId()) // - .withOfficeId(savingsAccount.officeId()) // - .withClientId(savingsAccount.clientId()) // - .withGroupId(savingsAccount.groupId()) // - .withSavingsId(savingsAccountId) // - .build(); - } - - @Transactional - @Override - public CommandProcessingResult updateSavingsAccountCharge(final JsonCommand command, final DepositAccountType depositAccountType) { - - this.context.authenticatedUser(); - this.savingsAccountChargeDataValidator.validateUpdate(command.json()); - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(SAVINGS_ACCOUNT_RESOURCE_NAME); - - final Long savingsAccountId = command.getSavingsId(); - // SavingsAccount Charge entity - final Long savingsChargeId = command.entityId(); - - final SavingsAccount savingsAccount = this.depositAccountAssembler.assembleFrom(savingsAccountId, depositAccountType); - checkClientOrGroupActive(savingsAccount); - - final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository.findOneWithNotFoundDetection(savingsChargeId, - savingsAccountId); - - final Map changes = savingsAccountCharge.update(command); - - if (savingsAccountCharge.getDueDate() != null) { - final Locale locale = command.extractLocale(); - final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); - - // transaction date should not be on a holiday or non working day - if (!this.configurationDomainService.allowTransactionsOnHolidayEnabled() - && this.holidayRepository.isHoliday(savingsAccount.officeId(), savingsAccountCharge.getDueDate())) { - baseDataValidator.reset().parameter(dueAsOfDateParamName).value(savingsAccountCharge.getDueDate().format(fmt)) - .failWithCodeNoParameterAddedToErrorCode("charge.due.date.is.on.holiday"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - if (!this.configurationDomainService.allowTransactionsOnNonWorkingDayEnabled() - && !this.workingDaysRepository.isWorkingDay(savingsAccountCharge.getDueDate())) { - baseDataValidator.reset().parameter(dueAsOfDateParamName).value(savingsAccountCharge.getDueDate().format(fmt)) - .failWithCodeNoParameterAddedToErrorCode("charge.due.date.is.a.nonworking.day"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - } - - this.savingsAccountChargeRepository.saveAndFlush(savingsAccountCharge); - - return new CommandProcessingResultBuilder() // - .withEntityId(savingsAccountCharge.getId()) // - .withOfficeId(savingsAccountCharge.savingsAccount().officeId()) // - .withClientId(savingsAccountCharge.savingsAccount().clientId()) // - .withGroupId(savingsAccountCharge.savingsAccount().groupId()) // - .withSavingsId(savingsAccountCharge.savingsAccount().getId()) // - .with(changes) // - .build(); - } - - @Transactional - @Override - public CommandProcessingResult waiveCharge(final Long savingsAccountId, final Long savingsAccountChargeId, - @SuppressWarnings("unused") final DepositAccountType depositAccountType) { - context.authenticatedUser(); - - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - - final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository - .findOneWithNotFoundDetection(savingsAccountChargeId, savingsAccountId); - - // Get Savings account from savings charge - final SavingsAccount account = savingsAccountCharge.savingsAccount(); - this.depositAccountAssembler.assignSavingAccountHelpers(account); - - final Set existingTransactionIds = new HashSet<>(); - final Set existingReversedTransactionIds = new HashSet<>(); - updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); - - account.waiveCharge(savingsAccountChargeId, false); - boolean isInterestTransfer = false; - LocalDate postInterestOnDate = null; - final MathContext mc = MathContext.DECIMAL64; - final boolean postReversals = false; - if (account.isBeforeLastPostingPeriod(savingsAccountCharge.getDueDate(), false)) { - final LocalDate today = DateUtils.getBusinessLocalDate(); - account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, - postInterestOnDate, false, postReversals); - } else { - final LocalDate today = DateUtils.getBusinessLocalDate(); - account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate, false, postReversals); - } - List depositAccountOnHoldTransactions = null; - if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { - depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository - .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); - } - - account.validateAccountBalanceDoesNotBecomeNegative(SavingsApiConstants.waiveChargeTransactionAction, - depositAccountOnHoldTransactions, false); - - this.savingAccountRepositoryWrapper.saveAndFlush(account); - - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds); - - return new CommandProcessingResultBuilder() // - .withEntityId(savingsAccountChargeId) // - .withOfficeId(account.officeId()) // - .withClientId(account.clientId()) // - .withGroupId(account.groupId()) // - .withSavingsId(savingsAccountId) // - .build(); - } - - @Transactional - @Override - public CommandProcessingResult deleteSavingsAccountCharge(final Long savingsAccountId, final Long savingsAccountChargeId, - @SuppressWarnings("unused") final JsonCommand command, final DepositAccountType depositAccountType) { - this.context.authenticatedUser(); - - final SavingsAccount savingsAccount = this.depositAccountAssembler.assembleFrom(savingsAccountId, depositAccountType); - checkClientOrGroupActive(savingsAccount); - final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository - .findOneWithNotFoundDetection(savingsAccountChargeId, savingsAccountId); - - savingsAccount.removeCharge(savingsAccountCharge); - this.savingAccountRepositoryWrapper.saveAndFlush(savingsAccount); - - return new CommandProcessingResultBuilder() // - .withEntityId(savingsAccountChargeId) // - .withOfficeId(savingsAccount.officeId()) // - .withClientId(savingsAccount.clientId()) // - .withGroupId(savingsAccount.groupId()) // - .withSavingsId(savingsAccountId) // - .build(); - } - - @Override - public CommandProcessingResult payCharge(final Long savingsAccountId, final Long savingsAccountChargeId, final JsonCommand command, - @SuppressWarnings("unused") final DepositAccountType depositAccountType) { - - this.context.authenticatedUser(); - - this.savingsAccountChargeDataValidator.validatePayCharge(command.json()); - final Locale locale = command.extractLocale(); - final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); - final BigDecimal amountPaid = command.bigDecimalValueOfParameterNamed(amountParamName); - final LocalDate transactionDate = command.localDateValueOfParameterNamed(dueAsOfDateParamName); - - final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository - .findOneWithNotFoundDetection(savingsAccountChargeId, savingsAccountId); - - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(SAVINGS_ACCOUNT_RESOURCE_NAME); - - // transaction date should not be on a holiday or non working day - if (!this.configurationDomainService.allowTransactionsOnHolidayEnabled() - && this.holidayRepository.isHoliday(savingsAccountCharge.savingsAccount().officeId(), transactionDate)) { - baseDataValidator.reset().parameter(dueAsOfDateParamName).value(transactionDate.format(fmt)) - .failWithCodeNoParameterAddedToErrorCode("transaction.not.allowed.transaction.date.is.on.holiday"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - if (!this.configurationDomainService.allowTransactionsOnNonWorkingDayEnabled() - && !this.workingDaysRepository.isWorkingDay(transactionDate)) { - baseDataValidator.reset().parameter(dueAsOfDateParamName).value(transactionDate.format(fmt)) - .failWithCodeNoParameterAddedToErrorCode("transaction.not.allowed.transaction.date.is.a.nonworking.day"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - this.payCharge(savingsAccountCharge, transactionDate, amountPaid, fmt); - return new CommandProcessingResultBuilder() // - .withEntityId(savingsAccountCharge.getId()) // - .withOfficeId(savingsAccountCharge.savingsAccount().officeId()) // - .withClientId(savingsAccountCharge.savingsAccount().clientId()) // - .withGroupId(savingsAccountCharge.savingsAccount().groupId()) // - .withSavingsId(savingsAccountCharge.savingsAccount().getId()) // - .build(); - - } - - @Transactional - @Override - public void applyChargeDue(final Long savingsAccountChargeId, final Long accountId, - @SuppressWarnings("unused") final DepositAccountType depositAccountType) { - final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository - .findOneWithNotFoundDetection(savingsAccountChargeId, accountId); - // always use current date as transaction date for batch job - final LocalDate transactionDate = DateUtils.getBusinessLocalDate(); - final DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd MM yyyy"); - - while (DateUtils.isBefore(savingsAccountCharge.getDueDate(), transactionDate)) { - payCharge(savingsAccountCharge, transactionDate, savingsAccountCharge.amoutOutstanding(), fmt); - } - } - - @Transactional - private void payCharge(final SavingsAccountCharge savingsAccountCharge, final LocalDate transactionDate, final BigDecimal amountPaid, - final DateTimeFormatter formatter) { - context.authenticatedUser(); - - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - - // Get Savings account from savings charge - final SavingsAccount account = savingsAccountCharge.savingsAccount(); - this.depositAccountAssembler.assignSavingAccountHelpers(account); - final Set existingTransactionIds = new HashSet<>(); - final Set existingReversedTransactionIds = new HashSet<>(); - updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); - account.payCharge(savingsAccountCharge, amountPaid, transactionDate, formatter, false, null); - boolean isInterestTransfer = false; - LocalDate postInterestOnDate = null; - final MathContext mc = MathContext.DECIMAL64; - final boolean postReversals = false; - if (account.isBeforeLastPostingPeriod(transactionDate, false)) { - final LocalDate today = DateUtils.getBusinessLocalDate(); - account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, - postInterestOnDate, false, postReversals); - } else { - final LocalDate today = DateUtils.getBusinessLocalDate(); - account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate, false, postReversals); - } - List depositAccountOnHoldTransactions = null; - if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { - depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository - .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); - } - - account.validateAccountBalanceDoesNotBecomeNegative("." + SavingsAccountTransactionType.PAY_CHARGE.getCode(), - depositAccountOnHoldTransactions, false); - - this.savingAccountRepositoryWrapper.saveAndFlush(account); - - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds); - } - - @Transactional - @Override - public void updateMaturityDetails(Long depositAccountId, DepositAccountType depositAccountType) { - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - final boolean postReversals = false; - final SavingsAccount account = this.depositAccountAssembler.assembleFrom(depositAccountId, depositAccountType); - final Set existingTransactionIds = new HashSet<>(); - final Set existingReversedTransactionIds = new HashSet<>(); - updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); - - if (depositAccountType.isFixedDeposit()) { - ((FixedDepositAccount) account).updateMaturityStatus(isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth); - FixedDepositAccount fdAccount = ((FixedDepositAccount) account); - // handle maturity instructions - - if (fdAccount.isMatured() && (fdAccount.isReinvestOnClosure() || fdAccount.isTransferToSavingsOnClosure())) { - DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd"); - Map changes = new HashMap<>(); - final AppUser user = context.authenticatedUser(); - Long toSavingsId = fdAccount.getTransferToSavingsAccountId(); - this.depositAccountDomainService.handleFDAccountMaturityClosure(fdAccount, null, user, fmt, fdAccount.maturityDate(), - fdAccount.getOnAccountClosureId(), toSavingsId, "Apply maturity instructions", changes); - - if (changes.get("reinvestedDepositId") != null) { - Long reinvestedDepositId = (Long) changes.get("reinvestedDepositId"); - Money amountForDeposit = account.activateWithBalance(); - final FixedDepositAccount reinvestAccount = (FixedDepositAccount) this.depositAccountAssembler - .assembleFrom(reinvestedDepositId, DepositAccountType.FIXED_DEPOSIT); - Money activationChargeAmount = getActivationCharge(reinvestAccount); - if (activationChargeAmount.isGreaterThanZero()) { - payActivationCharge(reinvestAccount); - amountForDeposit = amountForDeposit.plus(activationChargeAmount); - } - this.depositAccountDomainService.handleFDDeposit(reinvestAccount, fmt, fdAccount.maturityDate(), - amountForDeposit.getAmount(), null); - } - } - } else if (depositAccountType.isRecurringDeposit()) { - ((RecurringDepositAccount) account).updateMaturityStatus(isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postReversals); - } - this.savingAccountRepositoryWrapper.saveAndFlush(account); - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds); - } - - private void updateExistingTransactionsDetails(SavingsAccount account, Set existingTransactionIds, - Set existingReversedTransactionIds) { - existingTransactionIds.addAll(account.findExistingTransactionIds()); - existingReversedTransactionIds.addAll(account.findExistingReversedTransactionIds()); - } - - private void postJournalEntries(final SavingsAccount savingsAccount, final Set existingTransactionIds, - final Set existingReversedTransactionIds) { - - boolean isAccountTransfer = false; - final Map accountingBridgeData = savingsAccount.deriveAccountingBridgeData(savingsAccount.getCurrency().getCode(), - existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, false); - this.journalEntryWritePlatformService.createJournalEntriesForSavings(accountingBridgeData); - } - - @Transactional - @Override - public SavingsAccountTransaction mandatorySavingsAccountDeposit(final SavingsAccountTransactionDTO accountTransactionDTO) { - boolean isRegularTransaction = false; - final PaymentDetail paymentDetail = accountTransactionDTO.getPaymentDetail(); - if (paymentDetail != null && paymentDetail.getId() == null) { - this.paymentDetailWritePlatformService.persistPaymentDetail(paymentDetail); - } - if (accountTransactionDTO.getAccountType().equals(DepositAccountType.RECURRING_DEPOSIT.getValue())) { - RecurringDepositAccount account = (RecurringDepositAccount) this.depositAccountAssembler - .assembleFrom(accountTransactionDTO.getSavingsAccountId(), DepositAccountType.RECURRING_DEPOSIT); - return this.depositAccountDomainService.handleRDDeposit(account, accountTransactionDTO.getFormatter(), - accountTransactionDTO.getTransactionDate(), accountTransactionDTO.getTransactionAmount(), paymentDetail, - isRegularTransaction); - } - SavingsAccount account = null; - if (accountTransactionDTO.getAccountType().equals(DepositAccountType.SAVINGS_DEPOSIT.getValue())) { - account = this.depositAccountAssembler.assembleFrom(accountTransactionDTO.getSavingsAccountId(), - DepositAccountType.SAVINGS_DEPOSIT); - } else { - account = this.depositAccountAssembler.assembleFrom(accountTransactionDTO.getSavingsAccountId(), - DepositAccountType.CURRENT_DEPOSIT); - } - return this.depositAccountDomainService.handleSavingDeposit(account, accountTransactionDTO.getFormatter(), - accountTransactionDTO.getTransactionDate(), accountTransactionDTO.getTransactionAmount(), paymentDetail, - isRegularTransaction); - - } -} +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.savings.service; + +import static org.apache.fineract.portfolio.savings.DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.amountParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.chargeIdParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.dueAsOfDateParamName; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +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 java.util.UUID; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService; +import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.ApiParameterError; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; +import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; +import org.apache.fineract.infrastructure.core.domain.ExternalId; +import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; +import org.apache.fineract.infrastructure.core.exception.PlatformServiceUnavailableException; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.organisation.holiday.domain.HolidayRepositoryWrapper; +import org.apache.fineract.organisation.monetary.domain.ApplicationCurrencyRepositoryWrapper; +import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.organisation.monetary.domain.MoneyHelper; +import org.apache.fineract.organisation.office.domain.Office; +import org.apache.fineract.organisation.staff.domain.Staff; +import org.apache.fineract.organisation.workingdays.domain.WorkingDaysRepositoryWrapper; +import org.apache.fineract.portfolio.account.PortfolioAccountType; +import org.apache.fineract.portfolio.account.data.AccountTransferDTO; +import org.apache.fineract.portfolio.account.data.PortfolioAccountData; +import org.apache.fineract.portfolio.account.domain.AccountTransferType; +import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlatformService; +import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService; +import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService; +import org.apache.fineract.portfolio.calendar.domain.Calendar; +import org.apache.fineract.portfolio.calendar.domain.CalendarEntityType; +import org.apache.fineract.portfolio.calendar.domain.CalendarFrequencyType; +import org.apache.fineract.portfolio.calendar.domain.CalendarInstance; +import org.apache.fineract.portfolio.calendar.domain.CalendarInstanceRepository; +import org.apache.fineract.portfolio.calendar.domain.CalendarType; +import org.apache.fineract.portfolio.calendar.service.CalendarUtils; +import org.apache.fineract.portfolio.charge.domain.Charge; +import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper; +import org.apache.fineract.portfolio.client.domain.Client; +import org.apache.fineract.portfolio.client.exception.ClientNotActiveException; +import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; +import org.apache.fineract.portfolio.denominationmaster.service.DenominationBreakdownWriter; +import org.apache.fineract.portfolio.group.domain.Group; +import org.apache.fineract.portfolio.group.exception.GroupNotActiveException; +import org.apache.fineract.portfolio.note.domain.Note; +import org.apache.fineract.portfolio.note.domain.NoteRepository; +import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail; +import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService; +import org.apache.fineract.portfolio.savings.DepositAccountType; +import org.apache.fineract.portfolio.savings.DepositsApiConstants; +import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType; +import org.apache.fineract.portfolio.savings.SavingsApiConstants; +import org.apache.fineract.portfolio.savings.data.DepositAccountTransactionDataValidator; +import org.apache.fineract.portfolio.savings.data.SavingsAccountChargeDataValidator; +import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionDTO; +import org.apache.fineract.portfolio.savings.domain.DepositAccountAssembler; +import org.apache.fineract.portfolio.savings.domain.DepositAccountDomainService; +import org.apache.fineract.portfolio.savings.domain.DepositAccountOnHoldTransaction; +import org.apache.fineract.portfolio.savings.domain.DepositAccountOnHoldTransactionRepository; +import org.apache.fineract.portfolio.savings.domain.DepositAccountRecurringDetail; +import org.apache.fineract.portfolio.savings.domain.FixedDepositAccount; +import org.apache.fineract.portfolio.savings.domain.RecurringDepositAccount; +import org.apache.fineract.portfolio.savings.domain.SavingsAccount; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountCharge; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountChargeRepositoryWrapper; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountStatusType; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransactionRepository; +import org.apache.fineract.portfolio.savings.exception.DepositAccountTransactionNotAllowedException; +import org.apache.fineract.portfolio.savings.exception.SavingsAccountTransactionNotFoundException; +import org.apache.fineract.portfolio.savings.exception.TransactionUpdateNotAllowedException; +import org.apache.fineract.useradministration.domain.AppUser; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Transactional +@RequiredArgsConstructor +public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements DepositAccountWritePlatformService { + + private final PlatformSecurityContext context; + private final SavingsAccountRepositoryWrapper savingAccountRepositoryWrapper; + private final SavingsAccountTransactionRepository savingsAccountTransactionRepository; + private final DepositAccountAssembler depositAccountAssembler; + private final DepositAccountTransactionDataValidator depositAccountTransactionDataValidator; + private final SavingsAccountChargeDataValidator savingsAccountChargeDataValidator; + private final PaymentDetailWritePlatformService paymentDetailWritePlatformService; + private final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepositoryWrapper; + private final JournalEntryWritePlatformService journalEntryWritePlatformService; + private final DepositAccountDomainService depositAccountDomainService; + private final NoteRepository noteRepository; + private final AccountTransfersReadPlatformService accountTransfersReadPlatformService; + private final ChargeRepositoryWrapper chargeRepository; + private final SavingsAccountChargeRepositoryWrapper savingsAccountChargeRepository; + private final AccountAssociationsReadPlatformService accountAssociationsReadPlatformService; + private final AccountTransfersWritePlatformService accountTransfersWritePlatformService; + private final DepositAccountReadPlatformService depositAccountReadPlatformService; + private final CalendarInstanceRepository calendarInstanceRepository; + private final ConfigurationDomainService configurationDomainService; + private final HolidayRepositoryWrapper holidayRepository; + private final WorkingDaysRepositoryWrapper workingDaysRepository; + private final DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository; + private final DenominationBreakdownWriter denominationBreakdownWriter; + + @Transactional + @Override + public CommandProcessingResult activateFDAccount(final Long savingsId, final JsonCommand command) { + final AppUser user = context.authenticatedUser(); + + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + final boolean postReversals = false; + this.depositAccountTransactionDataValidator.validateActivation(command); + final MathContext mc = MathContext.DECIMAL64; + final FixedDepositAccount account = (FixedDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId, + DepositAccountType.FIXED_DEPOSIT); + checkClientOrGroupActive(account); + + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + + final Map changes = account.activate(user, command); + Money activationChargeAmount = getActivationCharge(account); + if (!changes.isEmpty()) { + final Locale locale = command.extractLocale(); + final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); + Money amountForDeposit = account.activateWithBalance().plus(activationChargeAmount); + if (amountForDeposit.isGreaterThanZero()) { + final PortfolioAccountData portfolioAccountData = this.accountAssociationsReadPlatformService + .retriveSavingsLinkedAssociation(savingsId); + + if (portfolioAccountData == null) { + final PaymentDetail paymentDetail = null; + this.depositAccountDomainService.handleFDDeposit(account, fmt, account.getActivationDate(), + amountForDeposit.getAmount(), paymentDetail); + } else { + final SavingsAccount fromSavingsAccount = null; + boolean isRegularTransaction = false; + final boolean isExceptionForBalanceCheck = false; + final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(account.getActivationDate(), + amountForDeposit.getAmount(), PortfolioAccountType.SAVINGS, PortfolioAccountType.SAVINGS, + portfolioAccountData.getId(), account.getId(), "Account Transfer", locale, fmt, null, null, null, null, null, + AccountTransferType.ACCOUNT_TRANSFER.getValue(), null, null, ExternalId.empty(), null, account, + fromSavingsAccount, isRegularTransaction, isExceptionForBalanceCheck); + this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO); + } + final boolean isInterestTransfer = false; + final LocalDate postInterestOnDate = null; + if (activationChargeAmount.isGreaterThanZero()) { + payActivationCharge(account); + } + if (account.isBeforeLastPostingPeriod(account.getActivationDate(), false)) { + final LocalDate today = DateUtils.getBusinessLocalDate(); + account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth, postInterestOnDate, false); + } else { + final LocalDate today = DateUtils.getBusinessLocalDate(); + account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth, postInterestOnDate, false, postReversals); + } + } + + final boolean isPreMatureClosure = false; + account.updateMaturityDateAndAmount(mc, isPreMatureClosure, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth); + List depositAccountOnHoldTransactions = null; + if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { + depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository + .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); + } + account.validateAccountBalanceDoesNotBecomeNegative(SavingsAccountTransactionType.PAY_CHARGE.name(), + depositAccountOnHoldTransactions, false); + this.savingAccountRepositoryWrapper.saveAndFlush(account); + } + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds); + + return new CommandProcessingResultBuilder() // + .withEntityId(savingsId) // + .withOfficeId(account.officeId()) // + .withClientId(account.clientId()) // + .withGroupId(account.groupId()) // + .withSavingsId(savingsId) // + .with(changes) // + .build(); + } + + private Money getActivationCharge(final FixedDepositAccount account) { + Money activationChargeAmount = Money.zero(account.getCurrency()); + for (SavingsAccountCharge savingsAccountCharge : account.charges()) { + if (savingsAccountCharge.isSavingsActivation()) { + activationChargeAmount = activationChargeAmount.plus(savingsAccountCharge.getAmount(account.getCurrency())); + } + } + return activationChargeAmount; + } + + private void payActivationCharge(final FixedDepositAccount account) { + for (SavingsAccountCharge savingsAccountCharge : account.charges()) { + if (savingsAccountCharge.isSavingsActivation()) { + account.payCharge(savingsAccountCharge, savingsAccountCharge.getAmount(account.getCurrency()), account.getActivationDate(), + false, null); + } + } + } + + @Transactional + @Override + public CommandProcessingResult activateRDAccount(final Long savingsId, final JsonCommand command) { + boolean isRegularTransaction = false; + + final AppUser user = this.context.authenticatedUser(); + + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + final boolean postReversals = false; + this.depositAccountTransactionDataValidator.validateActivation(command); + + final RecurringDepositAccount account = (RecurringDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId, + DepositAccountType.RECURRING_DEPOSIT); + checkClientOrGroupActive(account); + + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + + final Map changes = account.activate(user, command); + + if (!changes.isEmpty()) { + final Locale locale = command.extractLocale(); + final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); + Money amountForDeposit = account.activateWithBalance(); + if (amountForDeposit.isGreaterThanZero()) { + final PortfolioAccountData portfolioAccountData = this.accountAssociationsReadPlatformService + .retriveSavingsLinkedAssociation(savingsId); + if (portfolioAccountData == null) { + this.depositAccountDomainService.handleRDDeposit(account, fmt, account.getActivationDate(), + amountForDeposit.getAmount(), null, isRegularTransaction); + } else { + final boolean isExceptionForBalanceCheck = false; + final SavingsAccount fromSavingsAccount = null; + final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(account.getActivationDate(), + amountForDeposit.getAmount(), PortfolioAccountType.SAVINGS, PortfolioAccountType.SAVINGS, + portfolioAccountData.getId(), account.getId(), "Account Transfer", locale, fmt, null, null, null, null, null, + AccountTransferType.ACCOUNT_TRANSFER.getValue(), null, null, ExternalId.empty(), null, account, + fromSavingsAccount, isRegularTransaction, isExceptionForBalanceCheck); + this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO); + } + } + + final MathContext mc = MathContext.DECIMAL64; + + // submitted and activation date are different then recalculate + // maturity date and schedule + if (!account.accountSubmittedAndActivationOnSameDate()) { + final boolean isPreMatureClosure = false; + final CalendarInstance calendarInstance = this.calendarInstanceRepository.findByEntityIdAndEntityTypeIdAndCalendarTypeId( + savingsId, CalendarEntityType.SAVINGS.getValue(), CalendarType.COLLECTION.getValue()); + + final Calendar calendar = calendarInstance.getCalendar(); + final PeriodFrequencyType frequencyType = CalendarFrequencyType.from(CalendarUtils.getFrequency(calendar.getRecurrence())); + Integer frequency = CalendarUtils.getInterval(calendar.getRecurrence()); + frequency = frequency == -1 ? 1 : frequency; + account.generateSchedule(frequencyType, frequency, calendar); + account.updateMaturityDateAndAmount(mc, isPreMatureClosure, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth); + } + + final LocalDate overdueUptoDate = DateUtils.getBusinessLocalDate(); + account.updateOverduePayments(overdueUptoDate); + final boolean isInterestTransfer = false; + final LocalDate postInterestOnDate = null; + if (account.isBeforeLastPostingPeriod(account.getActivationDate(), false)) { + final LocalDate today = DateUtils.getBusinessLocalDate(); + account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, + postInterestOnDate, false, postReversals); + } else { + final LocalDate today = DateUtils.getBusinessLocalDate(); + account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth, postInterestOnDate, false, postReversals); + } + List depositAccountOnHoldTransactions = null; + if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { + depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository + .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); + } + + account.validateAccountBalanceDoesNotBecomeNegative(SavingsAccountTransactionType.PAY_CHARGE.name(), + depositAccountOnHoldTransactions, false); + + this.savingAccountRepositoryWrapper.saveAndFlush(account); + } + + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds); + + return new CommandProcessingResultBuilder() // + .withEntityId(savingsId) // + .withOfficeId(account.officeId()) // + .withClientId(account.clientId()) // + .withGroupId(account.groupId()) // + .withSavingsId(savingsId) // + .with(changes) // + .build(); + } + + @Transactional + @Override + public CommandProcessingResult depositToFDAccount(final Long savingsId, @SuppressWarnings("unused") final JsonCommand command) { + // this.context.authenticatedUser(); + this.depositAccountTransactionDataValidator.validate(command, DepositAccountType.FIXED_DEPOSIT); + + final FixedDepositAccount account = (FixedDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId, + DepositAccountType.FIXED_DEPOSIT); + checkClientOrGroupActive(account); + + final Locale locale = command.extractLocale(); + final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); + + final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); + final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount"); + + final Map changes = new LinkedHashMap<>(); + final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); + this.denominationBreakdownWriter.validateBreakdown(command, transactionAmount, "DEPOSIT"); + final SavingsAccountTransaction deposit = this.depositAccountDomainService.handleFDDeposit(account, fmt, transactionDate, + transactionAmount, paymentDetail); + + this.denominationBreakdownWriter.save(command, deposit.getId(), account.getId(), account.getAccountNumber(), + account.clientId(), account.getClient() != null ? account.getClient().getCifNumber() : null, transactionDate, + "DEPOSIT"); + + return new CommandProcessingResultBuilder().withEntityId(deposit.getId()).withOfficeId(account.officeId()) + .withClientId(account.clientId()).withGroupId(account.groupId()).withSavingsId(savingsId).with(changes).build(); + + } + + @Transactional + @Override + public CommandProcessingResult updateDepositAmountForRDAccount(Long savingsId, JsonCommand command) { + this.depositAccountTransactionDataValidator.validateDepositAmountUpdate(command); + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + + final BigDecimal mandatoryRecommendedDepositAmount = command + .bigDecimalValueOfParameterNamed(DepositsApiConstants.mandatoryRecommendedDepositAmountParamName); + + final LocalDate depositAmountUpdateEffectiveFromDate = command + .localDateValueOfParameterNamed(DepositsApiConstants.effectiveDateParamName); + + final RecurringDepositAccount recurringDepositAccount = (RecurringDepositAccount) this.depositAccountAssembler + .assembleFrom(savingsId, DepositAccountType.RECURRING_DEPOSIT); + DepositAccountRecurringDetail recurringDetail = recurringDepositAccount.getRecurringDetail(); + Map changes = recurringDetail.updateMandatoryRecommendedDepositAmount(mandatoryRecommendedDepositAmount, + depositAmountUpdateEffectiveFromDate, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth); + + return new CommandProcessingResultBuilder() // + .withEntityId(savingsId) // + .withOfficeId(recurringDepositAccount.officeId()) // + .withClientId(recurringDepositAccount.clientId()) // + .withGroupId(recurringDepositAccount.groupId()) // + .withSavingsId(savingsId) // + .with(changes) // + .build(); + } + + @Transactional + @Override + public CommandProcessingResult depositToRDAccount(final Long savingsId, final JsonCommand command) { + boolean isRegularTransaction = true; + + this.depositAccountTransactionDataValidator.validate(command, DepositAccountType.RECURRING_DEPOSIT); + + final RecurringDepositAccount account = (RecurringDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId, + DepositAccountType.RECURRING_DEPOSIT); + checkClientOrGroupActive(account); + + final Locale locale = command.extractLocale(); + final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); + + final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); + final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount"); + + final Map changes = new LinkedHashMap<>(); + final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); + this.denominationBreakdownWriter.validateBreakdown(command, transactionAmount, "DEPOSIT"); + final SavingsAccountTransaction deposit = this.depositAccountDomainService.handleRDDeposit(account, fmt, transactionDate, + transactionAmount, paymentDetail, isRegularTransaction); + + this.denominationBreakdownWriter.save(command, deposit.getId(), account.getId(), account.getAccountNumber(), + account.clientId(), account.getClient() != null ? account.getClient().getCifNumber() : null, transactionDate, + "DEPOSIT"); + + return new CommandProcessingResultBuilder() // + .withEntityId(deposit.getId()) // + .withOfficeId(account.officeId()) // + .withClientId(account.clientId()) // + .withGroupId(account.groupId()) // + .withSavingsId(savingsId) // + .with(changes) // + .build(); + } + + private Long saveTransactionToGenerateTransactionId(final SavingsAccountTransaction transaction) { + this.savingsAccountTransactionRepository.saveAndFlush(transaction); + return transaction.getId(); + } + + @Transactional + @Override + public CommandProcessingResult withdrawal(final Long savingsId, final JsonCommand command, + final DepositAccountType depositAccountType) { + + boolean isRegularTransaction = true; + + this.depositAccountTransactionDataValidator.validate(command, depositAccountType); + + final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); + final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount"); + + final Locale locale = command.extractLocale(); + final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); + + final Map changes = new LinkedHashMap<>(); + final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); + + final SavingsAccount account = this.depositAccountAssembler.assembleFrom(savingsId, depositAccountType); + + checkClientOrGroupActive(account); + + final SavingsAccountTransaction withdrawal = this.depositAccountDomainService.handleWithdrawal(account, fmt, transactionDate, + transactionAmount, paymentDetail, true, isRegularTransaction); + + // Validate and save denomination breakdown for WITHDRAWAL if provided + this.denominationBreakdownWriter.validateBreakdown(command, transactionAmount, "WITHDRAWAL"); + this.denominationBreakdownWriter.save(command, withdrawal.getId(), account.getId(), account.getAccountNumber(), + account.clientId(), account.getClient() != null ? account.getClient().getCifNumber() : null, transactionDate, + "WITHDRAWAL"); + + return new CommandProcessingResultBuilder() // + .withEntityId(withdrawal.getId()) // + .withOfficeId(account.officeId()) // + .withClientId(account.clientId()) // + .withGroupId(account.groupId()) // + .withSavingsId(savingsId) // + .with(changes)// + .build(); + } + + @Transactional + @Override + public CommandProcessingResult calculateInterest(final Long savingsId, final DepositAccountType depositAccountType) { + + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + + final SavingsAccount account = this.depositAccountAssembler.assembleFrom(savingsId, depositAccountType); + checkClientOrGroupActive(account); + + final LocalDate today = DateUtils.getBusinessLocalDate(); + final boolean postReversals = false; + final MathContext mc = new MathContext(15, MoneyHelper.getRoundingMode()); + boolean isInterestTransfer = false; + LocalDate postInterestOnDate = null; + account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth, postInterestOnDate, false, postReversals); + + this.savingAccountRepositoryWrapper.save(account); + + return new CommandProcessingResultBuilder() // + .withEntityId(savingsId) // + .withOfficeId(account.officeId()) // + .withClientId(account.clientId()) // + .withGroupId(account.groupId()) // + .withSavingsId(savingsId) // + .build(); + } + + @Transactional + @Override + public CommandProcessingResult postInterest(final Long savingsId, final DepositAccountType depositAccountType) { + + final SavingsAccount account = this.depositAccountAssembler.assembleFrom(savingsId, depositAccountType); + checkClientOrGroupActive(account); + postInterest(account); + return new CommandProcessingResultBuilder() // + .withEntityId(savingsId) // + .withOfficeId(account.officeId()) // + .withClientId(account.clientId()) // + .withGroupId(account.groupId()) // + .withSavingsId(savingsId) // + .build(); + } + + @Transactional + private void postInterest(final SavingsAccount account) { + + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + final LocalDate today = DateUtils.getBusinessLocalDate(); + final MathContext mc = new MathContext(10, MoneyHelper.getRoundingMode()); + boolean isInterestTransfer = false; + LocalDate postInterestOnDate = null; + final boolean postReversals = false; + account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, + postInterestOnDate, false, postReversals); + this.savingAccountRepositoryWrapper.saveAndFlush(account); + + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds); + } + + @Override + public CommandProcessingResult undoFDTransaction(final Long savingsId, @SuppressWarnings("unused") final Long transactionId, + @SuppressWarnings("unused") final boolean allowAccountTransferModification) { + + throw new DepositAccountTransactionNotAllowedException(savingsId, "undo", DepositAccountType.FIXED_DEPOSIT); + } + + @Override + public CommandProcessingResult undoRDTransaction(final Long savingsId, final Long transactionId, + final boolean allowAccountTransferModification) { + + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + + final RecurringDepositAccount account = (RecurringDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId, + DepositAccountType.RECURRING_DEPOSIT); + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + + final SavingsAccountTransaction savingsAccountTransaction = this.savingsAccountTransactionRepository + .findOneByIdAndSavingsAccountId(transactionId, savingsId); + if (savingsAccountTransaction == null) { + throw new SavingsAccountTransactionNotFoundException(savingsId, transactionId); + } + + if (!allowAccountTransferModification + && this.accountTransfersReadPlatformService.isAccountTransfer(transactionId, PortfolioAccountType.SAVINGS)) { + throw new PlatformServiceUnavailableException("error.msg.recurring.deposit.account.transfer.transaction.update.not.allowed", + "Recurring deposit account transaction:" + transactionId + " update not allowed as it involves in account transfer", + transactionId); + } + + final LocalDate today = DateUtils.getBusinessLocalDate(); + final MathContext mc = MathContext.DECIMAL64; + + if (account.isNotActive()) { + throwValidationForActiveStatus(SavingsApiConstants.undoTransactionAction); + } + account.undoTransaction(transactionId); + boolean isInterestTransfer = false; + LocalDate postInterestOnDate = null; + checkClientOrGroupActive(account); + final boolean postReversals = false; + if (savingsAccountTransaction.isPostInterestCalculationRequired() + && account.isBeforeLastPostingPeriod(savingsAccountTransaction.getTransactionDate(), false)) { + account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, + postInterestOnDate, false, postReversals); + } else { + account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth, postInterestOnDate, false, postReversals); + } + List depositAccountOnHoldTransactions = null; + if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { + depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository + .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); + } + + account.validateAccountBalanceDoesNotBecomeNegative(SavingsApiConstants.undoTransactionAction, depositAccountOnHoldTransactions, + false); + // account.activateAccountBasedOnBalance(); + final boolean isPreMatureClosure = false; + account.updateMaturityDateAndAmount(mc, isPreMatureClosure, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth); + + final LocalDate overdueUptoDate = DateUtils.getBusinessLocalDate(); + + if (savingsAccountTransaction.isDeposit()) { + account.updateScheduleInstallments(); + } + + account.updateOverduePayments(overdueUptoDate); + + this.savingAccountRepositoryWrapper.saveAndFlush(account); + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds); + + return new CommandProcessingResultBuilder() // + .withEntityId(savingsId) // + .withOfficeId(account.officeId()) // + .withClientId(account.clientId()) // + .withGroupId(account.groupId()) // + .withSavingsId(savingsId) // + .build(); + } + + @Override + public CommandProcessingResult adjustFDTransaction(final Long savingsId, @SuppressWarnings("unused") final Long transactionId, + @SuppressWarnings("unused") final JsonCommand command) { + + throw new DepositAccountTransactionNotAllowedException(savingsId, "modify", DepositAccountType.FIXED_DEPOSIT); + } + + @Override + public CommandProcessingResult adjustRDTransaction(final Long savingsId, final Long transactionId, final JsonCommand command) { + context.authenticatedUser(); + + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + final Long relaxingDaysConfigForPivotDate = this.configurationDomainService.retrieveRelaxingDaysConfigForPivotDate(); + this.depositAccountTransactionDataValidator.validate(command, DepositAccountType.RECURRING_DEPOSIT); + + final SavingsAccountTransaction savingsAccountTransaction = this.savingsAccountTransactionRepository + .findOneByIdAndSavingsAccountId(transactionId, savingsId); + if (savingsAccountTransaction == null) { + throw new SavingsAccountTransactionNotFoundException(savingsId, transactionId); + } + + if ((!savingsAccountTransaction.isDeposit() && !savingsAccountTransaction.isWithdrawal()) + || savingsAccountTransaction.isReversed()) { + throw new TransactionUpdateNotAllowedException(savingsId, transactionId); + } + + if (this.accountTransfersReadPlatformService.isAccountTransfer(transactionId, PortfolioAccountType.SAVINGS)) { + throw new PlatformServiceUnavailableException("error.msg.saving.account.transfer.transaction.update.not.allowed", + "Deposit account transaction:" + transactionId + " update not allowed as it involves in account transfer", + transactionId); + } + + final LocalDate today = DateUtils.getBusinessLocalDate(); + + final RecurringDepositAccount account = (RecurringDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId, + DepositAccountType.RECURRING_DEPOSIT); + if (account.isNotActive()) { + throwValidationForActiveStatus(SavingsApiConstants.adjustTransactionAction); + } + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + + final Locale locale = command.extractLocale(); + final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); + final LocalDate transactionDate = command.localDateValueOfParameterNamed(SavingsApiConstants.transactionDateParamName); + final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed(SavingsApiConstants.transactionAmountParamName); + final Map changes = new LinkedHashMap<>(); + final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); + + final MathContext mc = new MathContext(10, MoneyHelper.getRoundingMode()); + account.undoTransaction(transactionId); + + SavingsAccountTransaction transaction = null; + Integer accountType = null; + UUID refNo = UUID.randomUUID(); + if (savingsAccountTransaction.isDeposit()) { + final SavingsAccountTransactionDTO transactionDTO = new SavingsAccountTransactionDTO(fmt, transactionDate, transactionAmount, + paymentDetail, null, accountType); + transaction = account.deposit(transactionDTO, false, relaxingDaysConfigForPivotDate, refNo.toString()); + } else { + final SavingsAccountTransactionDTO transactionDTO = new SavingsAccountTransactionDTO(fmt, transactionDate, transactionAmount, + paymentDetail, null, accountType); + transaction = account.withdraw(transactionDTO, true, false, relaxingDaysConfigForPivotDate, refNo.toString()); + } + final Long newtransactionId = saveTransactionToGenerateTransactionId(transaction); + boolean isInterestTransfer = false; + final LocalDate postInterestOnDate = null; + final boolean postReversals = false; + if (account.isBeforeLastPostingPeriod(transactionDate, false) + || account.isBeforeLastPostingPeriod(savingsAccountTransaction.getTransactionDate(), false)) { + account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, + postInterestOnDate, false, postReversals); + } else { + account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth, postInterestOnDate, false, postReversals); + } + List depositAccountOnHoldTransactions = null; + if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { + depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository + .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); + } + + account.validateAccountBalanceDoesNotBecomeNegative(SavingsApiConstants.adjustTransactionAction, depositAccountOnHoldTransactions, + false); + account.activateAccountBasedOnBalance(); + + if (savingsAccountTransaction.isDeposit()) { + account.handleScheduleInstallments(savingsAccountTransaction); + } + final LocalDate overdueUptoDate = DateUtils.getBusinessLocalDate(); + account.updateOverduePayments(overdueUptoDate); + + this.savingAccountRepositoryWrapper.saveAndFlush(account); + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds); + return new CommandProcessingResultBuilder() // + .withEntityId(newtransactionId) // + .withOfficeId(account.officeId()) // + .withClientId(account.clientId()) // + .withGroupId(account.groupId()) // + .withSavingsId(savingsId) // + .with(changes)// + .build(); + } + + /** + * + */ + private void throwValidationForActiveStatus(final String actionName) { + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(SAVINGS_ACCOUNT_RESOURCE_NAME + actionName); + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("account.is.not.active"); + throw new PlatformApiDataValidationException(dataValidationErrors); + } + + private void checkClientOrGroupActive(final SavingsAccount account) { + final Client client = account.getClient(); + if (client != null && client.isNotActive()) { + throw new ClientNotActiveException(client.getId()); + } + final Group group = account.group(); + if (group != null && group.isNotActive()) { + throw new GroupNotActiveException(group.getId()); + } + } + + @Override + public CommandProcessingResult closeFDAccount(final Long savingsId, final JsonCommand command) { + final AppUser user = this.context.authenticatedUser(); + final boolean isPreMatureClose = false; + this.depositAccountTransactionDataValidator.validateClosing(command, DepositAccountType.FIXED_DEPOSIT, isPreMatureClose); + + final Map changes = new LinkedHashMap<>(); + final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); + + final FixedDepositAccount account = (FixedDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId, + DepositAccountType.FIXED_DEPOSIT); + checkClientOrGroupActive(account); + + this.depositAccountDomainService.handleFDAccountClosure(account, paymentDetail, user, command, changes); + + final String noteText = command.stringValueOfParameterNamed("note"); + if (StringUtils.isNotBlank(noteText)) { + final Note note = Note.savingNote(account, noteText); + changes.put("note", noteText); + this.noteRepository.save(note); + } + + return new CommandProcessingResultBuilder() // + .withEntityId(savingsId) // + .withOfficeId(account.officeId()) // + .withClientId(account.clientId()) // + .withGroupId(account.groupId()) // + .withSavingsId(savingsId) // + .with(changes)// + .build(); + + } + + @Override + public CommandProcessingResult closeRDAccount(final Long savingsId, final JsonCommand command) { + final AppUser user = this.context.authenticatedUser(); + + this.depositAccountTransactionDataValidator.validateClosing(command, DepositAccountType.RECURRING_DEPOSIT, false); + + final Map changes = new LinkedHashMap<>(); + final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); + + final RecurringDepositAccount account = (RecurringDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId, + DepositAccountType.RECURRING_DEPOSIT); + checkClientOrGroupActive(account); + + this.depositAccountDomainService.handleRDAccountClosure(account, paymentDetail, user, command, changes); + + final String noteText = command.stringValueOfParameterNamed("note"); + if (StringUtils.isNotBlank(noteText)) { + final Note note = Note.savingNote(account, noteText); + changes.put("note", noteText); + this.noteRepository.save(note); + } + + return new CommandProcessingResultBuilder() // + .withEntityId(savingsId) // + .withOfficeId(account.officeId()) // + .withClientId(account.clientId()) // + .withGroupId(account.groupId()) // + .withSavingsId(savingsId) // + .with(changes)// + .build(); + + } + + @Override + public CommandProcessingResult prematureCloseFDAccount(final Long savingsId, final JsonCommand command) { + final AppUser user = this.context.authenticatedUser(); + + this.depositAccountTransactionDataValidator.validateClosing(command, DepositAccountType.FIXED_DEPOSIT, true); + + final Map changes = new LinkedHashMap<>(); + final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); + + final FixedDepositAccount account = (FixedDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId, + DepositAccountType.FIXED_DEPOSIT); + checkClientOrGroupActive(account); + + this.depositAccountDomainService.handleFDAccountPreMatureClosure(account, paymentDetail, user, command, changes); + + final String noteText = command.stringValueOfParameterNamed("note"); + if (StringUtils.isNotBlank(noteText)) { + final Note note = Note.savingNote(account, noteText); + changes.put("note", noteText); + this.noteRepository.save(note); + } + + return new CommandProcessingResultBuilder() // + .withEntityId(savingsId) // + .withOfficeId(account.officeId()) // + .withClientId(account.clientId()) // + .withGroupId(account.groupId()) // + .withSavingsId(savingsId) // + .with(changes)// + .build(); + + } + + @Override + public CommandProcessingResult prematureCloseRDAccount(final Long savingsId, final JsonCommand command) { + final AppUser user = this.context.authenticatedUser(); + + this.depositAccountTransactionDataValidator.validateClosing(command, DepositAccountType.RECURRING_DEPOSIT, true); + + final Map changes = new LinkedHashMap<>(); + final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); + + final RecurringDepositAccount account = (RecurringDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId, + DepositAccountType.RECURRING_DEPOSIT); + checkClientOrGroupActive(account); + if (account.maturityDate() == null) { + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME + DepositsApiConstants.preMatureCloseAction); + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("can.not.close.as.premature"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + this.depositAccountDomainService.handleRDAccountPreMatureClosure(account, paymentDetail, user, command, changes); + + final String noteText = command.stringValueOfParameterNamed("note"); + if (StringUtils.isNotBlank(noteText)) { + final Note note = Note.savingNote(account, noteText); + changes.put("note", noteText); + this.noteRepository.save(note); + } + + return new CommandProcessingResultBuilder() // + .withEntityId(savingsId) // + .withOfficeId(account.officeId()) // + .withClientId(account.clientId()) // + .withGroupId(account.groupId()) // + .withSavingsId(savingsId) // + .with(changes)// + .build(); + + } + + @Override + public SavingsAccountTransaction initiateSavingsTransfer(final Long accountId, final LocalDate transferDate, + final DepositAccountType depositAccountType) { + context.authenticatedUser(); + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + + final SavingsAccount savingsAccount = this.depositAccountAssembler.assembleFrom(accountId, depositAccountType); + final LocalDate postInterestOnDate = null; + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + updateExistingTransactionsDetails(savingsAccount, existingTransactionIds, existingReversedTransactionIds); + + final SavingsAccountTransaction newTransferTransaction = SavingsAccountTransaction.initiateTransfer(savingsAccount, + savingsAccount.office(), transferDate); + savingsAccount.addTransaction(newTransferTransaction); + savingsAccount.setStatus(SavingsAccountStatusType.TRANSFER_IN_PROGRESS.getValue()); + final MathContext mc = MathContext.DECIMAL64; + boolean isInterestTransfer = false; + final boolean postReversals = false; + savingsAccount.calculateInterestUsing(mc, transferDate, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth, postInterestOnDate, false, postReversals); + + this.savingsAccountTransactionRepository.save(newTransferTransaction); + this.savingAccountRepositoryWrapper.saveAndFlush(savingsAccount); + + postJournalEntries(savingsAccount, existingTransactionIds, existingReversedTransactionIds); + + return newTransferTransaction; + } + + @Override + public SavingsAccountTransaction withdrawSavingsTransfer(final Long accountId, final LocalDate transferDate, + final DepositAccountType depositAccountType) { + context.authenticatedUser(); + + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + + final SavingsAccount savingsAccount = this.depositAccountAssembler.assembleFrom(accountId, depositAccountType); + + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + updateExistingTransactionsDetails(savingsAccount, existingTransactionIds, existingReversedTransactionIds); + + final SavingsAccountTransaction withdrawtransferTransaction = SavingsAccountTransaction.withdrawTransfer(savingsAccount, + savingsAccount.office(), transferDate); + savingsAccount.addTransaction(withdrawtransferTransaction); + savingsAccount.setStatus(SavingsAccountStatusType.ACTIVE.getValue()); + final boolean postReversals = false; + final MathContext mc = MathContext.DECIMAL64; + boolean isInterestTransfer = false; + LocalDate postInterestOnDate = null; + savingsAccount.calculateInterestUsing(mc, transferDate, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth, postInterestOnDate, false, postReversals); + + this.savingsAccountTransactionRepository.save(withdrawtransferTransaction); + this.savingAccountRepositoryWrapper.saveAndFlush(savingsAccount); + + postJournalEntries(savingsAccount, existingTransactionIds, existingReversedTransactionIds); + + return withdrawtransferTransaction; + } + + @Override + public void rejectSavingsTransfer(final Long accountId, final DepositAccountType depositAccountType) { + final SavingsAccount savingsAccount = this.depositAccountAssembler.assembleFrom(accountId, depositAccountType); + savingsAccount.setStatus(SavingsAccountStatusType.TRANSFER_ON_HOLD.getValue()); + this.savingAccountRepositoryWrapper.save(savingsAccount); + } + + @Override + public SavingsAccountTransaction acceptSavingsTransfer(final Long accountId, final LocalDate transferDate, + final Office acceptedInOffice, final Staff fieldOfficer, final DepositAccountType depositAccountType) { + context.authenticatedUser(); + + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + + final SavingsAccount savingsAccount = this.depositAccountAssembler.assembleFrom(accountId, depositAccountType); + + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + updateExistingTransactionsDetails(savingsAccount, existingTransactionIds, existingReversedTransactionIds); + + final SavingsAccountTransaction acceptTransferTransaction = SavingsAccountTransaction.approveTransfer(savingsAccount, + acceptedInOffice, transferDate); + savingsAccount.addTransaction(acceptTransferTransaction); + savingsAccount.setStatus(SavingsAccountStatusType.ACTIVE.getValue()); + if (fieldOfficer != null) { + savingsAccount.reassignSavingsOfficer(fieldOfficer, transferDate); + } + boolean isInterestTransfer = false; + final boolean postReversals = false; + LocalDate postInterestOnDate = null; + final MathContext mc = MathContext.DECIMAL64; + savingsAccount.calculateInterestUsing(mc, transferDate, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth, postInterestOnDate, false, postReversals); + + this.savingsAccountTransactionRepository.save(acceptTransferTransaction); + this.savingAccountRepositoryWrapper.saveAndFlush(savingsAccount); + + postJournalEntries(savingsAccount, existingTransactionIds, existingReversedTransactionIds); + + return acceptTransferTransaction; + } + + @Transactional + @Override + public CommandProcessingResult addSavingsAccountCharge(final JsonCommand command, final DepositAccountType depositAccountType) { + + this.context.authenticatedUser(); + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(SAVINGS_ACCOUNT_RESOURCE_NAME); + + final Long savingsAccountId = command.getSavingsId(); + this.savingsAccountChargeDataValidator.validateAdd(command.json()); + + final SavingsAccount savingsAccount = this.depositAccountAssembler.assembleFrom(savingsAccountId, depositAccountType); + checkClientOrGroupActive(savingsAccount); + + final Locale locale = command.extractLocale(); + final String format = command.dateFormat(); + final DateTimeFormatter fmt = StringUtils.isNotBlank(format) ? DateTimeFormatter.ofPattern(format).withLocale(locale) + : DateTimeFormatter.ofPattern("dd MM yyyy"); + + final Long chargeDefinitionId = command.longValueOfParameterNamed(chargeIdParamName); + final Charge chargeDefinition = this.chargeRepository.findOneWithNotFoundDetection(chargeDefinitionId); + + final SavingsAccountCharge savingsAccountCharge = SavingsAccountCharge.createNewFromJson(savingsAccount, chargeDefinition, command); + + if (savingsAccountCharge.getDueDate() != null) { + // transaction date should not be on a holiday or non working day + if (!this.configurationDomainService.allowTransactionsOnHolidayEnabled() + && this.holidayRepository.isHoliday(savingsAccount.officeId(), savingsAccountCharge.getDueDate())) { + baseDataValidator.reset().parameter(dueAsOfDateParamName).value(savingsAccountCharge.getDueDate().format(fmt)) + .failWithCodeNoParameterAddedToErrorCode("charge.due.date.is.on.holiday"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + if (!this.configurationDomainService.allowTransactionsOnNonWorkingDayEnabled() + && !this.workingDaysRepository.isWorkingDay(savingsAccountCharge.getDueDate())) { + baseDataValidator.reset().parameter(dueAsOfDateParamName).value(savingsAccountCharge.getDueDate().format(fmt)) + .failWithCodeNoParameterAddedToErrorCode("charge.due.date.is.a.nonworking.day"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + } + + savingsAccount.addCharge(fmt, savingsAccountCharge, chargeDefinition); + + this.savingAccountRepositoryWrapper.saveAndFlush(savingsAccount); + + return new CommandProcessingResultBuilder() // + .withEntityId(savingsAccountCharge.getId()) // + .withOfficeId(savingsAccount.officeId()) // + .withClientId(savingsAccount.clientId()) // + .withGroupId(savingsAccount.groupId()) // + .withSavingsId(savingsAccountId) // + .build(); + } + + @Transactional + @Override + public CommandProcessingResult updateSavingsAccountCharge(final JsonCommand command, final DepositAccountType depositAccountType) { + + this.context.authenticatedUser(); + this.savingsAccountChargeDataValidator.validateUpdate(command.json()); + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(SAVINGS_ACCOUNT_RESOURCE_NAME); + + final Long savingsAccountId = command.getSavingsId(); + // SavingsAccount Charge entity + final Long savingsChargeId = command.entityId(); + + final SavingsAccount savingsAccount = this.depositAccountAssembler.assembleFrom(savingsAccountId, depositAccountType); + checkClientOrGroupActive(savingsAccount); + + final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository.findOneWithNotFoundDetection(savingsChargeId, + savingsAccountId); + + final Map changes = savingsAccountCharge.update(command); + + if (savingsAccountCharge.getDueDate() != null) { + final Locale locale = command.extractLocale(); + final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); + + // transaction date should not be on a holiday or non working day + if (!this.configurationDomainService.allowTransactionsOnHolidayEnabled() + && this.holidayRepository.isHoliday(savingsAccount.officeId(), savingsAccountCharge.getDueDate())) { + baseDataValidator.reset().parameter(dueAsOfDateParamName).value(savingsAccountCharge.getDueDate().format(fmt)) + .failWithCodeNoParameterAddedToErrorCode("charge.due.date.is.on.holiday"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + if (!this.configurationDomainService.allowTransactionsOnNonWorkingDayEnabled() + && !this.workingDaysRepository.isWorkingDay(savingsAccountCharge.getDueDate())) { + baseDataValidator.reset().parameter(dueAsOfDateParamName).value(savingsAccountCharge.getDueDate().format(fmt)) + .failWithCodeNoParameterAddedToErrorCode("charge.due.date.is.a.nonworking.day"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + } + + this.savingsAccountChargeRepository.saveAndFlush(savingsAccountCharge); + + return new CommandProcessingResultBuilder() // + .withEntityId(savingsAccountCharge.getId()) // + .withOfficeId(savingsAccountCharge.savingsAccount().officeId()) // + .withClientId(savingsAccountCharge.savingsAccount().clientId()) // + .withGroupId(savingsAccountCharge.savingsAccount().groupId()) // + .withSavingsId(savingsAccountCharge.savingsAccount().getId()) // + .with(changes) // + .build(); + } + + @Transactional + @Override + public CommandProcessingResult waiveCharge(final Long savingsAccountId, final Long savingsAccountChargeId, + @SuppressWarnings("unused") final DepositAccountType depositAccountType) { + context.authenticatedUser(); + + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + + final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository + .findOneWithNotFoundDetection(savingsAccountChargeId, savingsAccountId); + + // Get Savings account from savings charge + final SavingsAccount account = savingsAccountCharge.savingsAccount(); + this.depositAccountAssembler.assignSavingAccountHelpers(account); + + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + + account.waiveCharge(savingsAccountChargeId, false); + boolean isInterestTransfer = false; + LocalDate postInterestOnDate = null; + final MathContext mc = MathContext.DECIMAL64; + final boolean postReversals = false; + if (account.isBeforeLastPostingPeriod(savingsAccountCharge.getDueDate(), false)) { + final LocalDate today = DateUtils.getBusinessLocalDate(); + account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, + postInterestOnDate, false, postReversals); + } else { + final LocalDate today = DateUtils.getBusinessLocalDate(); + account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth, postInterestOnDate, false, postReversals); + } + List depositAccountOnHoldTransactions = null; + if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { + depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository + .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); + } + + account.validateAccountBalanceDoesNotBecomeNegative(SavingsApiConstants.waiveChargeTransactionAction, + depositAccountOnHoldTransactions, false); + + this.savingAccountRepositoryWrapper.saveAndFlush(account); + + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds); + + return new CommandProcessingResultBuilder() // + .withEntityId(savingsAccountChargeId) // + .withOfficeId(account.officeId()) // + .withClientId(account.clientId()) // + .withGroupId(account.groupId()) // + .withSavingsId(savingsAccountId) // + .build(); + } + + @Transactional + @Override + public CommandProcessingResult deleteSavingsAccountCharge(final Long savingsAccountId, final Long savingsAccountChargeId, + @SuppressWarnings("unused") final JsonCommand command, final DepositAccountType depositAccountType) { + this.context.authenticatedUser(); + + final SavingsAccount savingsAccount = this.depositAccountAssembler.assembleFrom(savingsAccountId, depositAccountType); + checkClientOrGroupActive(savingsAccount); + final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository + .findOneWithNotFoundDetection(savingsAccountChargeId, savingsAccountId); + + savingsAccount.removeCharge(savingsAccountCharge); + this.savingAccountRepositoryWrapper.saveAndFlush(savingsAccount); + + return new CommandProcessingResultBuilder() // + .withEntityId(savingsAccountChargeId) // + .withOfficeId(savingsAccount.officeId()) // + .withClientId(savingsAccount.clientId()) // + .withGroupId(savingsAccount.groupId()) // + .withSavingsId(savingsAccountId) // + .build(); + } + + @Override + public CommandProcessingResult payCharge(final Long savingsAccountId, final Long savingsAccountChargeId, final JsonCommand command, + @SuppressWarnings("unused") final DepositAccountType depositAccountType) { + + this.context.authenticatedUser(); + + this.savingsAccountChargeDataValidator.validatePayCharge(command.json()); + final Locale locale = command.extractLocale(); + final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); + final BigDecimal amountPaid = command.bigDecimalValueOfParameterNamed(amountParamName); + final LocalDate transactionDate = command.localDateValueOfParameterNamed(dueAsOfDateParamName); + + final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository + .findOneWithNotFoundDetection(savingsAccountChargeId, savingsAccountId); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(SAVINGS_ACCOUNT_RESOURCE_NAME); + + // transaction date should not be on a holiday or non working day + if (!this.configurationDomainService.allowTransactionsOnHolidayEnabled() + && this.holidayRepository.isHoliday(savingsAccountCharge.savingsAccount().officeId(), transactionDate)) { + baseDataValidator.reset().parameter(dueAsOfDateParamName).value(transactionDate.format(fmt)) + .failWithCodeNoParameterAddedToErrorCode("transaction.not.allowed.transaction.date.is.on.holiday"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + if (!this.configurationDomainService.allowTransactionsOnNonWorkingDayEnabled() + && !this.workingDaysRepository.isWorkingDay(transactionDate)) { + baseDataValidator.reset().parameter(dueAsOfDateParamName).value(transactionDate.format(fmt)) + .failWithCodeNoParameterAddedToErrorCode("transaction.not.allowed.transaction.date.is.a.nonworking.day"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + this.payCharge(savingsAccountCharge, transactionDate, amountPaid, fmt); + return new CommandProcessingResultBuilder() // + .withEntityId(savingsAccountCharge.getId()) // + .withOfficeId(savingsAccountCharge.savingsAccount().officeId()) // + .withClientId(savingsAccountCharge.savingsAccount().clientId()) // + .withGroupId(savingsAccountCharge.savingsAccount().groupId()) // + .withSavingsId(savingsAccountCharge.savingsAccount().getId()) // + .build(); + + } + + @Transactional + @Override + public void applyChargeDue(final Long savingsAccountChargeId, final Long accountId, + @SuppressWarnings("unused") final DepositAccountType depositAccountType) { + final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository + .findOneWithNotFoundDetection(savingsAccountChargeId, accountId); + // always use current date as transaction date for batch job + final LocalDate transactionDate = DateUtils.getBusinessLocalDate(); + final DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd MM yyyy"); + + while (DateUtils.isBefore(savingsAccountCharge.getDueDate(), transactionDate)) { + payCharge(savingsAccountCharge, transactionDate, savingsAccountCharge.amoutOutstanding(), fmt); + } + } + + @Transactional + private void payCharge(final SavingsAccountCharge savingsAccountCharge, final LocalDate transactionDate, final BigDecimal amountPaid, + final DateTimeFormatter formatter) { + context.authenticatedUser(); + + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + + // Get Savings account from savings charge + final SavingsAccount account = savingsAccountCharge.savingsAccount(); + this.depositAccountAssembler.assignSavingAccountHelpers(account); + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + account.payCharge(savingsAccountCharge, amountPaid, transactionDate, formatter, false, null); + boolean isInterestTransfer = false; + LocalDate postInterestOnDate = null; + final MathContext mc = MathContext.DECIMAL64; + final boolean postReversals = false; + if (account.isBeforeLastPostingPeriod(transactionDate, false)) { + final LocalDate today = DateUtils.getBusinessLocalDate(); + account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, + postInterestOnDate, false, postReversals); + } else { + final LocalDate today = DateUtils.getBusinessLocalDate(); + account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth, postInterestOnDate, false, postReversals); + } + List depositAccountOnHoldTransactions = null; + if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { + depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository + .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); + } + + account.validateAccountBalanceDoesNotBecomeNegative("." + SavingsAccountTransactionType.PAY_CHARGE.getCode(), + depositAccountOnHoldTransactions, false); + + this.savingAccountRepositoryWrapper.saveAndFlush(account); + + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds); + } + + @Transactional + @Override + public void updateMaturityDetails(Long depositAccountId, DepositAccountType depositAccountType) { + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + final boolean postReversals = false; + final SavingsAccount account = this.depositAccountAssembler.assembleFrom(depositAccountId, depositAccountType); + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + + if (depositAccountType.isFixedDeposit()) { + ((FixedDepositAccount) account).updateMaturityStatus(isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth); + FixedDepositAccount fdAccount = ((FixedDepositAccount) account); + // handle maturity instructions + + if (fdAccount.isMatured() && (fdAccount.isReinvestOnClosure() || fdAccount.isTransferToSavingsOnClosure())) { + DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + Map changes = new HashMap<>(); + final AppUser user = context.authenticatedUser(); + Long toSavingsId = fdAccount.getTransferToSavingsAccountId(); + this.depositAccountDomainService.handleFDAccountMaturityClosure(fdAccount, null, user, fmt, fdAccount.maturityDate(), + fdAccount.getOnAccountClosureId(), toSavingsId, "Apply maturity instructions", changes); + + if (changes.get("reinvestedDepositId") != null) { + Long reinvestedDepositId = (Long) changes.get("reinvestedDepositId"); + Money amountForDeposit = account.activateWithBalance(); + final FixedDepositAccount reinvestAccount = (FixedDepositAccount) this.depositAccountAssembler + .assembleFrom(reinvestedDepositId, DepositAccountType.FIXED_DEPOSIT); + Money activationChargeAmount = getActivationCharge(reinvestAccount); + if (activationChargeAmount.isGreaterThanZero()) { + payActivationCharge(reinvestAccount); + amountForDeposit = amountForDeposit.plus(activationChargeAmount); + } + this.depositAccountDomainService.handleFDDeposit(reinvestAccount, fmt, fdAccount.maturityDate(), + amountForDeposit.getAmount(), null); + } + } + } else if (depositAccountType.isRecurringDeposit()) { + ((RecurringDepositAccount) account).updateMaturityStatus(isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth, postReversals); + } + this.savingAccountRepositoryWrapper.saveAndFlush(account); + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds); + } + + private void updateExistingTransactionsDetails(SavingsAccount account, Set existingTransactionIds, + Set existingReversedTransactionIds) { + existingTransactionIds.addAll(account.findExistingTransactionIds()); + existingReversedTransactionIds.addAll(account.findExistingReversedTransactionIds()); + } + + private void postJournalEntries(final SavingsAccount savingsAccount, final Set existingTransactionIds, + final Set existingReversedTransactionIds) { + + boolean isAccountTransfer = false; + final Map accountingBridgeData = savingsAccount.deriveAccountingBridgeData(savingsAccount.getCurrency().getCode(), + existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, false); + this.journalEntryWritePlatformService.createJournalEntriesForSavings(accountingBridgeData); + } + + @Transactional + @Override + public SavingsAccountTransaction mandatorySavingsAccountDeposit(final SavingsAccountTransactionDTO accountTransactionDTO) { + boolean isRegularTransaction = false; + final PaymentDetail paymentDetail = accountTransactionDTO.getPaymentDetail(); + if (paymentDetail != null && paymentDetail.getId() == null) { + this.paymentDetailWritePlatformService.persistPaymentDetail(paymentDetail); + } + if (accountTransactionDTO.getAccountType().equals(DepositAccountType.RECURRING_DEPOSIT.getValue())) { + RecurringDepositAccount account = (RecurringDepositAccount) this.depositAccountAssembler + .assembleFrom(accountTransactionDTO.getSavingsAccountId(), DepositAccountType.RECURRING_DEPOSIT); + return this.depositAccountDomainService.handleRDDeposit(account, accountTransactionDTO.getFormatter(), + accountTransactionDTO.getTransactionDate(), accountTransactionDTO.getTransactionAmount(), paymentDetail, + isRegularTransaction); + } + SavingsAccount account = null; + if (accountTransactionDTO.getAccountType().equals(DepositAccountType.SAVINGS_DEPOSIT.getValue())) { + account = this.depositAccountAssembler.assembleFrom(accountTransactionDTO.getSavingsAccountId(), + DepositAccountType.SAVINGS_DEPOSIT); + } else { + account = this.depositAccountAssembler.assembleFrom(accountTransactionDTO.getSavingsAccountId(), + DepositAccountType.CURRENT_DEPOSIT); + } + return this.depositAccountDomainService.handleSavingDeposit(account, accountTransactionDTO.getFormatter(), + accountTransactionDTO.getTransactionDate(), accountTransactionDTO.getTransactionAmount(), paymentDetail, + isRegularTransaction); + + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java index 7edfbf915..0ee80cdeb 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java @@ -88,8 +88,7 @@ import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlat import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService; import org.apache.fineract.portfolio.charge.domain.Charge; import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper; -import org.apache.fineract.portfolio.denominationmaster.domain.SavingsTxnDenomination; -import org.apache.fineract.portfolio.denominationmaster.domain.SavingsTxnDenominationRepository; +import org.apache.fineract.portfolio.denominationmaster.service.DenominationBreakdownWriter; import org.apache.fineract.portfolio.charge.domain.ChargeTimeType; import org.apache.fineract.portfolio.client.domain.Client; import org.apache.fineract.portfolio.client.exception.ClientNotActiveException; @@ -170,7 +169,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi private final SavingsAccountInterestPostingService savingsAccountInterestPostingService; private final ErrorHandler errorHandler; private final FacilityChargeAutoApplyService facilityChargeAutoApplyService; - private final SavingsTxnDenominationRepository savingsTxnDenominationRepository; + private final DenominationBreakdownWriter denominationBreakdownWriter; @Transactional @Override @@ -329,6 +328,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount"); this.savingsAccountTransactionDataValidator.validateTransactionWithPivotDate(transactionDate, account); + this.denominationBreakdownWriter.validateBreakdown(command, transactionAmount, "DEPOSIT"); final Map changes = new LinkedHashMap<>(); final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); @@ -337,38 +337,10 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(account, fmt, transactionDate, transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill); - // Save denomination breakdown if provided - if (command.parameterExists("denominationBreakdown")) { - final JsonArray denominationBreakdown = command.arrayOfParameterNamed("denominationBreakdown"); - if (denominationBreakdown != null && denominationBreakdown.size() > 0) { - final Long clientId = account.clientId(); - final String accountNo = account.getAccountNumber(); - final String cifNo = account.getExternalId() != null ? account.getExternalId().getValue() : null; - for (JsonElement entry : denominationBreakdown) { - final com.google.gson.JsonObject obj = entry.getAsJsonObject(); - if (!obj.has("denominationId") || !obj.has("count")) continue; - final long denominationId = obj.get("denominationId").getAsLong(); - final int count = obj.get("count").getAsInt(); - if (count <= 0) continue; - final int denominationValue = obj.has("denominationValue") ? obj.get("denominationValue").getAsInt() : 0; - final BigDecimal total = BigDecimal.valueOf((long) denominationValue * count); - final SavingsTxnDenomination row = SavingsTxnDenomination.builder() - .txnId(deposit.getId()) - .savingsAccountId(account.getId()) - .accountNo(accountNo) - .clientId(clientId) - .cifNo(cifNo) - .denominationId(denominationId) - .denominationValue(denominationValue) - .count(count) - .totalAmount(total) - .txnDate(transactionDate) - .createdAt(LocalDateTime.now()) - .build(); - this.savingsTxnDenominationRepository.save(row); - } - } - } + // Save denomination breakdown for DEPOSIT via shared writer + this.denominationBreakdownWriter.save(command, deposit.getId(), account.getId(), account.getAccountNumber(), + account.clientId(), account.getClient() != null ? account.getClient().getCifNumber() : null, + transactionDate, "DEPOSIT"); if (isGsim && (deposit.getId() != null)) { @@ -450,9 +422,14 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi BigDecimal currentBalance = gsim.getParentDeposit().subtract(transactionAmount); gsim.setParentDeposit(currentBalance); gsimRepository.save(gsim); - } + // Validate and save denomination breakdown for WITHDRAWAL via shared writer + this.denominationBreakdownWriter.validateBreakdown(command, transactionAmount, "WITHDRAWAL"); + this.denominationBreakdownWriter.save(command, withdrawal.getId(), account.getId(), account.getAccountNumber(), + account.clientId(), account.getClient() != null ? account.getClient().getCifNumber() : null, + transactionDate, "WITHDRAWAL"); + final String noteText = command.stringValueOfParameterNamed("note"); if (StringUtils.isNotBlank(noteText)) { final Note note = Note.savingsTransactionNote(account, withdrawal, noteText); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java index 2079c29e5..7b2cfdc32 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java @@ -108,6 +108,7 @@ import org.apache.fineract.portfolio.savings.service.DepositAccountPreMatureCalc import org.apache.fineract.portfolio.savings.service.DepositAccountPreMatureCalculationPlatformServiceImpl; import org.apache.fineract.portfolio.savings.service.DepositAccountReadPlatformService; import org.apache.fineract.portfolio.savings.service.DepositAccountReadPlatformServiceImpl; +import org.apache.fineract.portfolio.denominationmaster.service.DenominationBreakdownWriter; import org.apache.fineract.portfolio.savings.service.DepositAccountWritePlatformService; import org.apache.fineract.portfolio.savings.service.DepositAccountWritePlatformServiceJpaRepositoryImpl; import org.apache.fineract.portfolio.savings.service.DepositApplicationProcessWritePlatformService; @@ -268,7 +269,8 @@ public class SavingsConfiguration { DepositAccountReadPlatformService depositAccountReadPlatformService, CalendarInstanceRepository calendarInstanceRepository, ConfigurationDomainService configurationDomainService, HolidayRepositoryWrapper holidayRepository, WorkingDaysRepositoryWrapper workingDaysRepository, - DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository + DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository, + DenominationBreakdownWriter denominationBreakdownWriter ) { return new DepositAccountWritePlatformServiceJpaRepositoryImpl(context, savingAccountRepositoryWrapper, @@ -277,7 +279,8 @@ public class SavingsConfiguration { journalEntryWritePlatformService, depositAccountDomainService, noteRepository, accountTransfersReadPlatformService, chargeRepository, savingsAccountChargeRepository, accountAssociationsReadPlatformService, accountTransfersWritePlatformService, depositAccountReadPlatformService, calendarInstanceRepository, - configurationDomainService, holidayRepository, workingDaysRepository, depositAccountOnHoldTransactionRepository); + configurationDomainService, holidayRepository, workingDaysRepository, depositAccountOnHoldTransactionRepository, + denominationBreakdownWriter); } @Bean @@ -426,7 +429,7 @@ public class SavingsConfiguration { StandingInstructionRepository standingInstructionRepository, BusinessEventNotifierService businessEventNotifierService, GSIMRepositoy gsimRepository, SavingsAccountInterestPostingService savingsAccountInterestPostingService, ErrorHandler errorHandler, FacilityChargeAutoApplyService facilityChargeAutoApplyService, - SavingsTxnDenominationRepository savingsTxnDenominationRepository) { + DenominationBreakdownWriter denominationBreakdownWriter) { return new SavingsAccountWritePlatformServiceJpaRepositoryImpl(context, fromApiJsonDeserializer, savingAccountRepositoryWrapper, staffRepository, savingsAccountTransactionRepository, savingAccountAssembler, savingsAccountTransactionDataValidator, savingsAccountChargeDataValidator, paymentDetailWritePlatformService, journalEntryWritePlatformService, @@ -434,7 +437,7 @@ public class SavingsConfiguration { chargeRepository, savingsAccountChargeRepository, holidayRepository, workingDaysRepository, configurationDomainService, depositAccountOnHoldTransactionRepository, entityDatatableChecksWritePlatformService, appuserRepository, standingInstructionRepository, businessEventNotifierService, gsimRepository, savingsAccountInterestPostingService, - errorHandler, facilityChargeAutoApplyService, savingsTxnDenominationRepository); + errorHandler, facilityChargeAutoApplyService, denominationBreakdownWriter); } @Bean diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index ad7639229..7935a1da7 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -319,4 +319,5 @@ + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0012_denomination_master.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0012_denomination_master.xml index aad95f9a8..85fec5075 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0012_denomination_master.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0012_denomination_master.xml @@ -177,4 +177,28 @@ + + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0013_cash_exchange.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0013_cash_exchange.xml new file mode 100644 index 000000000..a4d48752a --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0013_cash_exchange.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From b89e653bcb29a028c52e82f7048d2af6520fce6f Mon Sep 17 00:00:00 2001 From: Jaanu423 Date: Thu, 28 May 2026 17:40:57 +0530 Subject: [PATCH 07/10] feat: cash management backend - transfer, settlement, inter-cashier, reconciliation + cashier /me position endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OVERVIEW -------- Added complete cash management backend covering the full cashier workflow: vault transfers, inter-cashier transfers, end-of-day settlement, GL reconciliation, and a new authenticated /me endpoint for cashier position lookup without needing to pass a cashier ID. NEW API ENDPOINT ---------------- GET /cashreports/cashier-position/me - Auto-detects the authenticated user via PlatformSecurityContext - Resolves appUser -> staff -> cashier via m_cashiers JOIN chain - Accepts optional ?asOfDate=YYYY-MM-DD query param (defaults to today) - Reuses existing buildPositionForCashier(cashierId, dateFilter) internally - Returns: { cashierId, asOfDate, totalAmount, denominations[] } - Non-cashier users get empty denominations array (no error thrown) - Used by Angular UI to show opening/closing denomination panels CASH TRANSFER (vault -> cashier) --------------------------------- CashTransferApiResource.java — REST endpoints (POST /cashtransfer, GET list/detail) CashTransfer.java — JPA entity (m_cash_transfer) CashTransferDetail.java — JPA entity (denomination breakdown, m_cash_transfer_detail) CashTransferRepository.java — Spring Data JPA repository CashTransferDetailRepository.java DB migrations: 0014_cash_transfer.xml — creates m_cash_transfer + m_cash_transfer_detail tables 0015_cash_transfer_add_ids.xml — adds missing ID columns to transfer detail CASH SETTLEMENT (end-of-day cashier close) ------------------------------------------ CashSettlementApiResource.java — REST endpoints (POST /cashsettlement, GET list/detail) CashSettlement.java — JPA entity (m_cash_settlement) CashSettlementDetail.java — JPA entity (denomination breakdown) CashSettlementRepository.java CashSettlementDetailRepository.java DB migration: 0016_cash_settlement.xml — creates m_cash_settlement + m_cash_settlement_detail INTER-CASHIER TRANSFER ----------------------- CashInterCashierApiResource.java — REST endpoints (POST /cashintercashier, GET list/detail) CashInterCashier.java — JPA entity (m_cash_inter_cashier) CashInterCashierDetail.java — JPA entity (denomination breakdown) CashInterCashierRepository.java CashInterCashierDetailRepository.java DB migration: 0017_cash_inter_cashier.xml — creates m_cash_inter_cashier + detail table CASH RECONCILIATION -------------------- CashReconciliationApiResource.java — REST endpoints (POST /cashreconciliation, GET list) CashReconciliation.java — JPA entity (m_cash_reconciliation) CashReconciliationDetail.java — JPA entity (denomination breakdown) CashReconciliationRepository.java CashReconciliationDetailRepository.java DB migrations: 0018_cash_reconciliation.xml — creates m_cash_reconciliation + detail table 0019_cash_reversal_columns.xml — adds reversal tracking columns 0020_cash_permissions.xml — adds permissions for all cash management APIs GL DISPATCH SERVICE ------------------- GlDispatchService.java (portfolio/teller/service/) - Shared service for posting GL journal entries for cash movements - Used by settlement and reconciliation flows CHANGELOG --------- changelog-tenant.xml updated to include migrations 0014 through 0020. Co-Authored-By: Claude Sonnet 4.6 --- .../api/CashInterCashierApiResource.java | 224 +++++++++++++ .../domain/CashInterCashier.java | 71 ++++ .../domain/CashInterCashierDetail.java | 39 +++ .../CashInterCashierDetailRepository.java | 10 + .../domain/CashInterCashierRepository.java | 11 + .../api/CashReconciliationApiResource.java | 310 ++++++++++++++++++ .../domain/CashReconciliation.java | 62 ++++ .../domain/CashReconciliationDetail.java | 45 +++ .../CashReconciliationDetailRepository.java | 10 + .../domain/CashReconciliationRepository.java | 11 + .../cashreport/api/CashReportApiResource.java | 291 ++++++++++++++++ .../api/CashSettlementApiResource.java | 254 ++++++++++++++ .../cashsettlement/domain/CashSettlement.java | 68 ++++ .../domain/CashSettlementDetail.java | 39 +++ .../CashSettlementDetailRepository.java | 10 + .../domain/CashSettlementRepository.java | 11 + .../api/CashTransferApiResource.java | 276 ++++++++++++++++ .../cashtransfer/domain/CashTransfer.java | 72 ++++ .../domain/CashTransferDetail.java | 43 +++ .../domain/CashTransferDetailRepository.java | 10 + .../domain/CashTransferRepository.java | 10 + .../teller/service/GlDispatchService.java | 57 ++++ .../db/changelog/tenant/changelog-tenant.xml | 7 + .../tenant/parts/0014_cash_transfer.xml | 48 +++ .../parts/0015_cash_transfer_add_ids.xml | 13 + .../tenant/parts/0016_cash_settlement.xml | 54 +++ .../tenant/parts/0017_cash_inter_cashier.xml | 55 ++++ .../tenant/parts/0018_cash_reconciliation.xml | 56 ++++ .../parts/0019_cash_reversal_columns.xml | 15 + .../tenant/parts/0020_cash_permissions.xml | 54 +++ 30 files changed, 2236 insertions(+) create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/cashintercashier/api/CashInterCashierApiResource.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/cashintercashier/domain/CashInterCashier.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/cashintercashier/domain/CashInterCashierDetail.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/cashintercashier/domain/CashInterCashierDetailRepository.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/cashintercashier/domain/CashInterCashierRepository.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreconciliation/api/CashReconciliationApiResource.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreconciliation/domain/CashReconciliation.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreconciliation/domain/CashReconciliationDetail.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreconciliation/domain/CashReconciliationDetailRepository.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreconciliation/domain/CashReconciliationRepository.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreport/api/CashReportApiResource.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/cashsettlement/api/CashSettlementApiResource.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/cashsettlement/domain/CashSettlement.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/cashsettlement/domain/CashSettlementDetail.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/cashsettlement/domain/CashSettlementDetailRepository.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/cashsettlement/domain/CashSettlementRepository.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/cashtransfer/api/CashTransferApiResource.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/cashtransfer/domain/CashTransfer.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/cashtransfer/domain/CashTransferDetail.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/cashtransfer/domain/CashTransferDetailRepository.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/cashtransfer/domain/CashTransferRepository.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/teller/service/GlDispatchService.java create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0014_cash_transfer.xml create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0015_cash_transfer_add_ids.xml create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0016_cash_settlement.xml create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0017_cash_inter_cashier.xml create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0018_cash_reconciliation.xml create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0019_cash_reversal_columns.xml create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0020_cash_permissions.xml diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashintercashier/api/CashInterCashierApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashintercashier/api/CashInterCashierApiResource.java new file mode 100644 index 000000000..39803e8f7 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashintercashier/api/CashInterCashierApiResource.java @@ -0,0 +1,224 @@ +package org.apache.fineract.portfolio.cashintercashier.api; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.portfolio.cashintercashier.domain.CashInterCashier; +import org.apache.fineract.portfolio.cashintercashier.domain.CashInterCashierDetail; +import org.apache.fineract.portfolio.cashintercashier.domain.CashInterCashierDetailRepository; +import org.apache.fineract.portfolio.cashintercashier.domain.CashInterCashierRepository; +import org.apache.fineract.useradministration.domain.AppUser; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Path("/v1/cashintercashier") +@Component +@RequiredArgsConstructor +@Consumes({ MediaType.APPLICATION_JSON }) +@Produces({ MediaType.APPLICATION_JSON }) +public class CashInterCashierApiResource { + + private final CashInterCashierRepository repository; + private final CashInterCashierDetailRepository detailRepository; + private final PlatformSecurityContext context; + private final Gson gson = new Gson(); + + @POST + @Transactional + public String create(String body) { + context.authenticatedUser().validateHasPermissionTo("CREATE_CASHINTERCASHIER"); + + JsonObject json = JsonParser.parseString(body).getAsJsonObject(); + + final Long tellerId = json.has("tellerId") && !json.get("tellerId").isJsonNull() + ? json.get("tellerId").getAsLong() : null; + final Long fromCashierId = json.has("fromCashierId") && !json.get("fromCashierId").isJsonNull() + ? json.get("fromCashierId").getAsLong() : null; + final Long toCashierId = json.has("toCashierId") && !json.get("toCashierId").isJsonNull() + ? json.get("toCashierId").getAsLong() : null; + + if (tellerId == null || fromCashierId == null || toCashierId == null) { + throw new PlatformDataIntegrityException("error.msg.cashintercashier.parties.required", + "tellerId, fromCashierId and toCashierId are required."); + } + if (fromCashierId.equals(toCashierId)) { + throw new PlatformDataIntegrityException("error.msg.cashintercashier.same.cashier", + "From-cashier and To-cashier cannot be the same."); + } + + final LocalDate transferDate = json.has("transferDate") && !json.get("transferDate").isJsonNull() + ? LocalDate.parse(json.get("transferDate").getAsString()) : LocalDate.now(); + final String fromParty = json.has("fromParty") && !json.get("fromParty").isJsonNull() + ? json.get("fromParty").getAsString() : null; + final String toParty = json.has("toParty") && !json.get("toParty").isJsonNull() + ? json.get("toParty").getAsString() : null; + final String note = json.has("note") && !json.get("note").isJsonNull() + ? json.get("note").getAsString() : null; + + final JsonArray denominations = json.has("denominations") && json.get("denominations").isJsonArray() + ? json.getAsJsonArray("denominations") : new JsonArray(); + + if (!hasPositiveCount(denominations)) { + throw new PlatformDataIntegrityException("error.msg.cashintercashier.empty", + "At least one denomination with a positive count is required."); + } + final BigDecimal total = sumTotal(denominations); + + CashInterCashier header = new CashInterCashier(); + header.setTransferDate(transferDate); + header.setTellerId(tellerId); + header.setFromCashierId(fromCashierId); + header.setToCashierId(toCashierId); + header.setFromParty(fromParty); + header.setToParty(toParty); + header.setTransferAmount(total); + header.setNote(note); + header.setCreatedAt(LocalDateTime.now()); + header.setIsReversed(false); + try { + AppUser user = this.context.authenticatedUser(); + header.setPerformedBy(user.getId()); + header.setPerformedByUsername(user.getUsername()); + } catch (Exception ignored) {} + CashInterCashier saved = repository.saveAndFlush(header); + saveDetails(saved.getId(), denominations); + + JsonObject result = new JsonObject(); + result.addProperty("resourceId", saved.getId()); + return gson.toJson(result); + } + + @POST + @Path("{id}/reverse") + @Transactional + public String reverse(@PathParam("id") Long id, String body) { + context.authenticatedUser().validateHasPermissionTo("REVERSE_CASHINTERCASHIER"); + + CashInterCashier record = repository.findById(id) + .orElseThrow(() -> new PlatformDataIntegrityException("error.msg.cashintercashier.not.found", + "Inter-cashier transfer not found: " + id)); + if (Boolean.TRUE.equals(record.getIsReversed())) { + throw new PlatformDataIntegrityException("error.msg.cashintercashier.already.reversed", + "Inter-cashier transfer " + id + " is already reversed."); + } + + String reason = null; + try { + JsonObject json = JsonParser.parseString(body).getAsJsonObject(); + reason = json.has("reason") ? json.get("reason").getAsString() : null; + } catch (Exception ignored) {} + + AppUser user = null; + try { user = this.context.authenticatedUser(); } catch (Exception ignored) {} + + record.setIsReversed(true); + record.setReversedAt(LocalDateTime.now()); + record.setReverseReason(reason); + if (user != null) record.setReversedBy(user.getId()); + repository.save(record); + + JsonObject result = new JsonObject(); + result.addProperty("resourceId", id); + result.addProperty("reversed", true); + return gson.toJson(result); + } + + @GET + public String getAll() { + return gson.toJson(repository.findAllByOrderByCreatedAtDesc().stream() + .map(this::toMap).collect(Collectors.toList())); + } + + @GET + @Path("{id}") + public String getOne(@PathParam("id") Long id) { + CashInterCashier header = repository.findById(id) + .orElseThrow(() -> new RuntimeException("Inter-cashier transfer not found: " + id)); + Map map = toMap(header); + map.put("details", detailRepository.findByInterCashierId(id).stream() + .map(this::detailToMap).collect(Collectors.toList())); + return gson.toJson(map); + } + + private BigDecimal sumTotal(JsonArray items) { + BigDecimal total = BigDecimal.ZERO; + for (JsonElement el : items) { + JsonObject item = el.getAsJsonObject(); + int value = item.has("denominationValue") ? item.get("denominationValue").getAsInt() : 0; + int count = item.has("count") ? item.get("count").getAsInt() : 0; + if (count > 0) total = total.add(BigDecimal.valueOf((long) value * count)); + } + return total; + } + + private boolean hasPositiveCount(JsonArray items) { + for (JsonElement el : items) { + if (el.getAsJsonObject().get("count").getAsInt() > 0) return true; + } + return false; + } + + private void saveDetails(Long interCashierId, JsonArray items) { + for (JsonElement el : items) { + JsonObject item = el.getAsJsonObject(); + int count = item.has("count") ? item.get("count").getAsInt() : 0; + if (count <= 0) continue; + int value = item.has("denominationValue") ? item.get("denominationValue").getAsInt() : 0; + Long denomId = item.has("denominationId") && !item.get("denominationId").isJsonNull() + ? item.get("denominationId").getAsLong() : null; + CashInterCashierDetail d = new CashInterCashierDetail(); + d.setInterCashierId(interCashierId); + d.setDenominationId(denomId); + d.setDenominationValue(value); + d.setCount(count); + d.setTotalAmount(BigDecimal.valueOf((long) value * count)); + detailRepository.save(d); + } + } + + private Map toMap(CashInterCashier e) { + Map m = new LinkedHashMap<>(); + m.put("id", e.getId()); + m.put("transferDate", e.getTransferDate() != null ? e.getTransferDate().toString() : null); + m.put("tellerId", e.getTellerId()); + m.put("fromCashierId", e.getFromCashierId()); + m.put("toCashierId", e.getToCashierId()); + m.put("fromParty", e.getFromParty()); + m.put("toParty", e.getToParty()); + m.put("transferAmount", e.getTransferAmount()); + m.put("note", e.getNote()); + m.put("performedByUsername", e.getPerformedByUsername()); + m.put("createdAt", e.getCreatedAt() != null ? e.getCreatedAt().toString() : null); + m.put("isReversed", e.getIsReversed()); + return m; + } + + private Map detailToMap(CashInterCashierDetail d) { + Map m = new LinkedHashMap<>(); + m.put("id", d.getId()); + m.put("denominationId", d.getDenominationId()); + m.put("denominationValue", d.getDenominationValue()); + m.put("count", d.getCount()); + m.put("totalAmount", d.getTotalAmount()); + return m; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashintercashier/domain/CashInterCashier.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashintercashier/domain/CashInterCashier.java new file mode 100644 index 000000000..b78020cb7 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashintercashier/domain/CashInterCashier.java @@ -0,0 +1,71 @@ +package org.apache.fineract.portfolio.cashintercashier.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Table(name = "m_cash_inter_cashier") +@Getter +@Setter +@NoArgsConstructor +public class CashInterCashier { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "transfer_date") + private LocalDate transferDate; + + @Column(name = "teller_id") + private Long tellerId; + + @Column(name = "from_cashier_id") + private Long fromCashierId; + + @Column(name = "to_cashier_id") + private Long toCashierId; + + @Column(name = "from_party", length = 100) + private String fromParty; + + @Column(name = "to_party", length = 100) + private String toParty; + + @Column(name = "transfer_amount", nullable = false) + private BigDecimal transferAmount; + + @Column(name = "note", length = 500) + private String note; + + @Column(name = "performed_by") + private Long performedBy; + + @Column(name = "performed_by_username", length = 100) + private String performedByUsername; + + @Column(name = "created_at") + private LocalDateTime createdAt; + + @Column(name = "is_reversed") + private Boolean isReversed = false; + + @Column(name = "reversed_by") + private Long reversedBy; + + @Column(name = "reversed_at") + private LocalDateTime reversedAt; + + @Column(name = "reverse_reason", length = 500) + private String reverseReason; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashintercashier/domain/CashInterCashierDetail.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashintercashier/domain/CashInterCashierDetail.java new file mode 100644 index 000000000..e2ba005f1 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashintercashier/domain/CashInterCashierDetail.java @@ -0,0 +1,39 @@ +package org.apache.fineract.portfolio.cashintercashier.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.math.BigDecimal; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Table(name = "m_cash_inter_cashier_detail") +@Getter +@Setter +@NoArgsConstructor +public class CashInterCashierDetail { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "inter_cashier_id", nullable = false) + private Long interCashierId; + + @Column(name = "denomination_id") + private Long denominationId; + + @Column(name = "denomination_value", nullable = false) + private Integer denominationValue; + + @Column(name = "count", nullable = false) + private Integer count; + + @Column(name = "total_amount", nullable = false) + private BigDecimal totalAmount; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashintercashier/domain/CashInterCashierDetailRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashintercashier/domain/CashInterCashierDetailRepository.java new file mode 100644 index 000000000..985f0c879 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashintercashier/domain/CashInterCashierDetailRepository.java @@ -0,0 +1,10 @@ +package org.apache.fineract.portfolio.cashintercashier.domain; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface CashInterCashierDetailRepository extends JpaRepository { + List findByInterCashierId(Long interCashierId); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashintercashier/domain/CashInterCashierRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashintercashier/domain/CashInterCashierRepository.java new file mode 100644 index 000000000..f8ee565e5 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashintercashier/domain/CashInterCashierRepository.java @@ -0,0 +1,11 @@ +package org.apache.fineract.portfolio.cashintercashier.domain; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface CashInterCashierRepository extends JpaRepository { + List findAllByOrderByCreatedAtDesc(); + List findByTellerIdOrderByCreatedAtDesc(Long tellerId); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreconciliation/api/CashReconciliationApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreconciliation/api/CashReconciliationApiResource.java new file mode 100644 index 000000000..69983ee93 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreconciliation/api/CashReconciliationApiResource.java @@ -0,0 +1,310 @@ +package org.apache.fineract.portfolio.cashreconciliation.api; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.portfolio.cashreconciliation.domain.CashReconciliation; +import org.apache.fineract.portfolio.cashreconciliation.domain.CashReconciliationDetail; +import org.apache.fineract.portfolio.cashreconciliation.domain.CashReconciliationDetailRepository; +import org.apache.fineract.portfolio.cashreconciliation.domain.CashReconciliationRepository; +import org.apache.fineract.useradministration.domain.AppUser; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Path("/v1/cashreconciliation") +@Component +@RequiredArgsConstructor +@Consumes({ MediaType.APPLICATION_JSON }) +@Produces({ MediaType.APPLICATION_JSON }) +public class CashReconciliationApiResource { + + private final CashReconciliationRepository repository; + private final CashReconciliationDetailRepository detailRepository; + private final PlatformSecurityContext context; + private final JdbcTemplate jdbcTemplate; + private final Gson gson = new Gson(); + + /** + * GET /cashreconciliation/expected?cashierId=X&asOfDate=yyyy-MM-dd + * Returns denomination-wise expected balance for a cashier. + * Computation: allocations (m_cash_transfer_detail where to_cashier_id=X) + * + inter-cashier-in (m_cash_inter_cashier_detail where to_cashier_id=X) + * - settlements (m_cash_settlement_detail where from_cashier_id=X) + * - inter-cashier-out (m_cash_inter_cashier_detail where from_cashier_id=X) + */ + @GET + @Path("expected") + public String getExpected(@QueryParam("cashierId") Long cashierId, @QueryParam("asOfDate") String asOfDate) { + context.authenticatedUser().validateHasPermissionTo("READ_CASHRECONCILIATION"); + if (cashierId == null) { + throw new PlatformDataIntegrityException("error.msg.cashreconciliation.cashier.required", + "cashierId query param is required."); + } + final String dateFilter = (asOfDate != null && !asOfDate.isBlank()) ? asOfDate : LocalDate.now().toString(); + + // In: vault → cashier allocations — GROUP BY denomination_id+kind to keep coin and note separate + final String sqlIn1 = "SELECT ctd.denomination_id, ctd.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(ctd.count) AS total_count " + + "FROM m_cash_transfer_detail ctd " + + "JOIN m_cash_transfer ct ON ct.id = ctd.transfer_id " + + "LEFT JOIN m_denomination_master dm ON dm.id = ctd.denomination_id " + + "WHERE ct.to_cashier_id = ? AND (ct.is_reversed IS NULL OR ct.is_reversed = false) " + + "AND ct.transfer_date <= CAST(? AS DATE) " + + "GROUP BY ctd.denomination_id, ctd.denomination_value, dm.kind"; + + // In: inter-cashier in + final String sqlIn2 = "SELECT icd.denomination_id, icd.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(icd.count) AS total_count " + + "FROM m_cash_inter_cashier_detail icd " + + "JOIN m_cash_inter_cashier ic ON ic.id = icd.inter_cashier_id " + + "LEFT JOIN m_denomination_master dm ON dm.id = icd.denomination_id " + + "WHERE ic.to_cashier_id = ? AND (ic.is_reversed IS NULL OR ic.is_reversed = false) " + + "AND ic.transfer_date <= CAST(? AS DATE) " + + "GROUP BY icd.denomination_id, icd.denomination_value, dm.kind"; + + // In: customer deposits received by this cashier + final String sqlIn3 = "SELECT std.denomination_id, std.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(std.count) AS total_count " + + "FROM m_savings_txn_denomination std " + + "LEFT JOIN m_denomination_master dm ON dm.id = std.denomination_id " + + "WHERE std.txn_type = 'DEPOSIT' " + + "AND std.txn_date <= CAST(? AS DATE) " + + "AND std.officer_id IN (" + + " SELECT au.id FROM m_appuser au " + + " JOIN m_cashiers mc ON mc.staff_id = au.staff_id WHERE mc.id = ?) " + + "GROUP BY std.denomination_id, std.denomination_value, dm.kind"; + + // Out: settlements to vault + final String sqlOut1 = "SELECT csd.denomination_id, csd.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(csd.count) AS total_count " + + "FROM m_cash_settlement_detail csd " + + "JOIN m_cash_settlement cs ON cs.id = csd.settlement_id " + + "LEFT JOIN m_denomination_master dm ON dm.id = csd.denomination_id " + + "WHERE cs.from_cashier_id = ? AND (cs.is_reversed IS NULL OR cs.is_reversed = false) " + + "AND cs.settlement_date <= CAST(? AS DATE) " + + "GROUP BY csd.denomination_id, csd.denomination_value, dm.kind"; + + // Out: inter-cashier out + final String sqlOut2 = "SELECT icd.denomination_id, icd.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(icd.count) AS total_count " + + "FROM m_cash_inter_cashier_detail icd " + + "JOIN m_cash_inter_cashier ic ON ic.id = icd.inter_cashier_id " + + "LEFT JOIN m_denomination_master dm ON dm.id = icd.denomination_id " + + "WHERE ic.from_cashier_id = ? AND (ic.is_reversed IS NULL OR ic.is_reversed = false) " + + "AND ic.transfer_date <= CAST(? AS DATE) " + + "GROUP BY icd.denomination_id, icd.denomination_value, dm.kind"; + + // Out: customer withdrawals paid by this cashier + final String sqlOut3 = "SELECT std.denomination_id, std.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(std.count) AS total_count " + + "FROM m_savings_txn_denomination std " + + "LEFT JOIN m_denomination_master dm ON dm.id = std.denomination_id " + + "WHERE std.txn_type = 'WITHDRAWAL' " + + "AND std.txn_date <= CAST(? AS DATE) " + + "AND std.officer_id IN (" + + " SELECT au.id FROM m_appuser au " + + " JOIN m_cashiers mc ON mc.staff_id = au.staff_id WHERE mc.id = ?) " + + "GROUP BY std.denomination_id, std.denomination_value, dm.kind"; + + // Balance map: denomination_id → long[3] {denomValue, kindFlag (0=note/1=coin), netCount} + Map balance = new LinkedHashMap<>(); + + addToBalance(balance, jdbcTemplate.query(sqlIn1, DENOM_ROW, cashierId, dateFilter), 1); + addToBalance(balance, jdbcTemplate.query(sqlIn2, DENOM_ROW, cashierId, dateFilter), 1); + addToBalance(balance, jdbcTemplate.query(sqlIn3, DENOM_ROW, dateFilter, cashierId), 1); + addToBalance(balance, jdbcTemplate.query(sqlOut1, DENOM_ROW, cashierId, dateFilter), -1); + addToBalance(balance, jdbcTemplate.query(sqlOut2, DENOM_ROW, cashierId, dateFilter), -1); + addToBalance(balance, jdbcTemplate.query(sqlOut3, DENOM_ROW, dateFilter, cashierId), -1); + + List> rows = new ArrayList<>(); + balance.entrySet().stream() + .sorted((a, b) -> Long.compare(b.getValue()[0], a.getValue()[0])) + .forEach(e -> { + Map row = new LinkedHashMap<>(); + row.put("denominationId", e.getKey()); + row.put("denominationValue", (int) e.getValue()[0]); + row.put("kind", e.getValue()[1] == 1L ? "coin" : "note"); + row.put("expectedCount", e.getValue()[2]); + row.put("expectedAmount", BigDecimal.valueOf(e.getValue()[0] * e.getValue()[2])); + rows.add(row); + }); + + BigDecimal grandTotal = rows.stream() + .map(r -> (BigDecimal) r.get("expectedAmount")) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + JsonObject result = new JsonObject(); + result.addProperty("cashierId", cashierId); + result.addProperty("asOfDate", dateFilter); + result.addProperty("expectedTotal", grandTotal); + result.add("denominations", gson.toJsonTree(rows)); + return gson.toJson(result); + } + + @POST + @Transactional + public String create(String body) { + context.authenticatedUser().validateHasPermissionTo("CREATE_CASHRECONCILIATION"); + + JsonObject json = JsonParser.parseString(body).getAsJsonObject(); + + final Long cashierId = json.has("cashierId") ? json.get("cashierId").getAsLong() : null; + if (cashierId == null) { + throw new PlatformDataIntegrityException("error.msg.cashreconciliation.cashier.required", + "cashierId is required."); + } + final LocalDate reconDate = json.has("reconDate") && !json.get("reconDate").isJsonNull() + ? LocalDate.parse(json.get("reconDate").getAsString()) : LocalDate.now(); + final String cashierName = json.has("cashierName") && !json.get("cashierName").isJsonNull() + ? json.get("cashierName").getAsString() : null; + final Long tellerId = json.has("tellerId") && !json.get("tellerId").isJsonNull() + ? json.get("tellerId").getAsLong() : null; + final String note = json.has("note") && !json.get("note").isJsonNull() + ? json.get("note").getAsString() : null; + + final JsonArray denominations = json.has("denominations") && json.get("denominations").isJsonArray() + ? json.getAsJsonArray("denominations") : new JsonArray(); + + BigDecimal expectedTotal = BigDecimal.ZERO; + BigDecimal physicalTotal = BigDecimal.ZERO; + List details = new ArrayList<>(); + + for (JsonElement el : denominations) { + JsonObject item = el.getAsJsonObject(); + int denomValue = item.has("denominationValue") ? item.get("denominationValue").getAsInt() : 0; + int expectedCount = item.has("expectedCount") ? item.get("expectedCount").getAsInt() : 0; + int physicalCount = item.has("physicalCount") ? item.get("physicalCount").getAsInt() : 0; + int varianceCount = physicalCount - expectedCount; + BigDecimal varianceAmount = BigDecimal.valueOf((long) denomValue * varianceCount); + expectedTotal = expectedTotal.add(BigDecimal.valueOf((long) denomValue * expectedCount)); + physicalTotal = physicalTotal.add(BigDecimal.valueOf((long) denomValue * physicalCount)); + + Long denomId = item.has("denominationId") && !item.get("denominationId").isJsonNull() + ? item.get("denominationId").getAsLong() : null; + + CashReconciliationDetail d = new CashReconciliationDetail(); + d.setDenominationId(denomId); + d.setDenominationValue(denomValue); + d.setExpectedCount(expectedCount); + d.setPhysicalCount(physicalCount); + d.setVarianceCount(varianceCount); + d.setVarianceAmount(varianceAmount); + details.add(d); + } + + BigDecimal variance = physicalTotal.subtract(expectedTotal); + String status = variance.compareTo(BigDecimal.ZERO) == 0 ? "OK" + : variance.compareTo(BigDecimal.ZERO) > 0 ? "EXCESS" : "SHORT"; + + CashReconciliation header = new CashReconciliation(); + header.setReconDate(reconDate); + header.setCashierId(cashierId); + header.setCashierName(cashierName); + header.setTellerId(tellerId); + header.setExpectedAmount(expectedTotal); + header.setPhysicalAmount(physicalTotal); + header.setVariance(variance); + header.setStatus(status); + header.setNote(note); + header.setCreatedAt(LocalDateTime.now()); + try { + AppUser user = this.context.authenticatedUser(); + header.setPerformedBy(user.getId()); + header.setPerformedByUsername(user.getUsername()); + } catch (Exception ignored) {} + CashReconciliation saved = repository.saveAndFlush(header); + + for (CashReconciliationDetail d : details) { + d.setReconciliationId(saved.getId()); + detailRepository.save(d); + } + + JsonObject result = new JsonObject(); + result.addProperty("resourceId", saved.getId()); + result.addProperty("status", status); + result.addProperty("variance", variance); + return gson.toJson(result); + } + + @GET + public String getAll() { + return gson.toJson(repository.findAllByOrderByCreatedAtDesc().stream() + .map(this::toMap).collect(Collectors.toList())); + } + + @GET + @Path("{id}") + public String getOne(@PathParam("id") Long id) { + CashReconciliation header = repository.findById(id) + .orElseThrow(() -> new RuntimeException("Cash reconciliation not found: " + id)); + Map map = toMap(header); + map.put("details", detailRepository.findByReconciliationId(id).stream() + .map(this::detailToMap).collect(Collectors.toList())); + return gson.toJson(map); + } + + // Row mapper: Object[]{Long denomId, Integer denomValue, String kind, Long count} + private static final org.springframework.jdbc.core.RowMapper DENOM_ROW = + (rs, r) -> new Object[] { + rs.getLong("denomination_id"), + rs.getInt("denomination_value"), + rs.getString("kind"), + rs.getLong("total_count") + }; + + private void addToBalance(Map balance, List rows, int sign) { + for (Object[] row : rows) { + Long denomId = (Long) row[0]; + int denomValue = (Integer) row[1]; + long kindFlag = "coin".equalsIgnoreCase((String) row[2]) ? 1L : 0L; + long count = (Long) row[3]; + long[] entry = balance.computeIfAbsent(denomId, k -> new long[] { denomValue, kindFlag, 0L }); + entry[2] += count * sign; + } + } + + private Map toMap(CashReconciliation e) { + Map m = new LinkedHashMap<>(); + m.put("id", e.getId()); + m.put("reconDate", e.getReconDate() != null ? e.getReconDate().toString() : null); + m.put("cashierId", e.getCashierId()); + m.put("cashierName", e.getCashierName()); + m.put("tellerId", e.getTellerId()); + m.put("expectedAmount", e.getExpectedAmount()); + m.put("physicalAmount", e.getPhysicalAmount()); + m.put("variance", e.getVariance()); + m.put("status", e.getStatus()); + m.put("note", e.getNote()); + m.put("performedByUsername", e.getPerformedByUsername()); + m.put("createdAt", e.getCreatedAt() != null ? e.getCreatedAt().toString() : null); + return m; + } + + private Map detailToMap(CashReconciliationDetail d) { + Map m = new LinkedHashMap<>(); + m.put("id", d.getId()); + m.put("denominationId", d.getDenominationId()); + m.put("denominationValue", d.getDenominationValue()); + m.put("expectedCount", d.getExpectedCount()); + m.put("physicalCount", d.getPhysicalCount()); + m.put("varianceCount", d.getVarianceCount()); + m.put("varianceAmount", d.getVarianceAmount()); + return m; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreconciliation/domain/CashReconciliation.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreconciliation/domain/CashReconciliation.java new file mode 100644 index 000000000..0deda4686 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreconciliation/domain/CashReconciliation.java @@ -0,0 +1,62 @@ +package org.apache.fineract.portfolio.cashreconciliation.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Table(name = "m_cash_reconciliation") +@Getter +@Setter +@NoArgsConstructor +public class CashReconciliation { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "recon_date", nullable = false) + private LocalDate reconDate; + + @Column(name = "cashier_id", nullable = false) + private Long cashierId; + + @Column(name = "cashier_name", length = 150) + private String cashierName; + + @Column(name = "teller_id") + private Long tellerId; + + @Column(name = "expected_amount") + private BigDecimal expectedAmount = BigDecimal.ZERO; + + @Column(name = "physical_amount") + private BigDecimal physicalAmount = BigDecimal.ZERO; + + @Column(name = "variance") + private BigDecimal variance = BigDecimal.ZERO; + + @Column(name = "status", length = 10) + private String status = "OK"; + + @Column(name = "note", length = 500) + private String note; + + @Column(name = "performed_by") + private Long performedBy; + + @Column(name = "performed_by_username", length = 100) + private String performedByUsername; + + @Column(name = "created_at") + private LocalDateTime createdAt; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreconciliation/domain/CashReconciliationDetail.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreconciliation/domain/CashReconciliationDetail.java new file mode 100644 index 000000000..3368ce0b0 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreconciliation/domain/CashReconciliationDetail.java @@ -0,0 +1,45 @@ +package org.apache.fineract.portfolio.cashreconciliation.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.math.BigDecimal; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Table(name = "m_cash_reconciliation_detail") +@Getter +@Setter +@NoArgsConstructor +public class CashReconciliationDetail { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "reconciliation_id", nullable = false) + private Long reconciliationId; + + @Column(name = "denomination_id") + private Long denominationId; + + @Column(name = "denomination_value", nullable = false) + private Integer denominationValue; + + @Column(name = "expected_count") + private Integer expectedCount = 0; + + @Column(name = "physical_count") + private Integer physicalCount = 0; + + @Column(name = "variance_count") + private Integer varianceCount = 0; + + @Column(name = "variance_amount") + private BigDecimal varianceAmount = BigDecimal.ZERO; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreconciliation/domain/CashReconciliationDetailRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreconciliation/domain/CashReconciliationDetailRepository.java new file mode 100644 index 000000000..7b626bf2d --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreconciliation/domain/CashReconciliationDetailRepository.java @@ -0,0 +1,10 @@ +package org.apache.fineract.portfolio.cashreconciliation.domain; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface CashReconciliationDetailRepository extends JpaRepository { + List findByReconciliationId(Long reconciliationId); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreconciliation/domain/CashReconciliationRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreconciliation/domain/CashReconciliationRepository.java new file mode 100644 index 000000000..02d326b27 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreconciliation/domain/CashReconciliationRepository.java @@ -0,0 +1,11 @@ +package org.apache.fineract.portfolio.cashreconciliation.domain; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface CashReconciliationRepository extends JpaRepository { + List findAllByOrderByCreatedAtDesc(); + List findByCashierIdOrderByReconDateDesc(Long cashierId); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreport/api/CashReportApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreport/api/CashReportApiResource.java new file mode 100644 index 000000000..f9c5c3031 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreport/api/CashReportApiResource.java @@ -0,0 +1,291 @@ +package org.apache.fineract.portfolio.cashreport.api; + +import com.google.gson.Gson; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Component; + +@Path("/v1/cashreports") +@Component +@RequiredArgsConstructor +@Produces({ MediaType.APPLICATION_JSON }) +public class CashReportApiResource { + + private final JdbcTemplate jdbcTemplate; + private final PlatformSecurityContext context; + private final Gson gson = new Gson(); + + @GET + @Path("closing") + public String closing(@QueryParam("date") String date) { + final String txnDate = (date != null && !date.isBlank()) ? date : LocalDate.now().toString(); + + // Single query — conditional aggregation for deposit, withdrawal, net per denomination + final String sql = "SELECT std.denomination_id, std.denomination_value, COALESCE(dm.kind, 'note') AS kind, " + + "SUM(CASE WHEN std.txn_type = 'DEPOSIT' THEN std.count ELSE 0 END) AS deposit_count, " + + "SUM(CASE WHEN std.txn_type = 'DEPOSIT' THEN std.total_amount ELSE 0 END) AS deposit_amount, " + + "SUM(CASE WHEN std.txn_type = 'WITHDRAWAL' THEN std.count ELSE 0 END) AS withdrawal_count, " + + "SUM(CASE WHEN std.txn_type = 'WITHDRAWAL' THEN std.total_amount ELSE 0 END) AS withdrawal_amount " + + "FROM m_savings_txn_denomination std " + + "LEFT JOIN m_denomination_master dm ON dm.id = std.denomination_id " + + "WHERE std.txn_date = CAST(? AS DATE) " + + "GROUP BY std.denomination_id, std.denomination_value, dm.kind " + + "ORDER BY std.denomination_value DESC, dm.kind"; + + final List> rows = jdbcTemplate.query(sql, (rs, rowNum) -> { + long depositCount = rs.getLong("deposit_count"); + long withdrawalCount = rs.getLong("withdrawal_count"); + BigDecimal depositAmount = rs.getBigDecimal("deposit_amount"); + BigDecimal withdrawalAmount = rs.getBigDecimal("withdrawal_amount"); + Map row = new LinkedHashMap<>(); + row.put("denominationId", rs.getLong("denomination_id")); + row.put("denominationValue", rs.getInt("denomination_value")); + row.put("kind", rs.getString("kind")); + row.put("depositCount", depositCount); + row.put("depositAmount", depositAmount); + row.put("withdrawalCount", withdrawalCount); + row.put("withdrawalAmount", withdrawalAmount); + row.put("netCount", depositCount - withdrawalCount); + row.put("netAmount", depositAmount.subtract(withdrawalAmount)); + return row; + }, txnDate); + + BigDecimal depositTotal = BigDecimal.ZERO; + BigDecimal withdrawalTotal = BigDecimal.ZERO; + for (Map row : rows) { + depositTotal = depositTotal.add((BigDecimal) row.get("depositAmount")); + withdrawalTotal = withdrawalTotal.add((BigDecimal) row.get("withdrawalAmount")); + } + + Map result = new LinkedHashMap<>(); + result.put("date", txnDate); + result.put("rows", rows); + result.put("depositTotal", depositTotal); + result.put("withdrawalTotal", withdrawalTotal); + result.put("netTotal", depositTotal.subtract(withdrawalTotal)); + return gson.toJson(result); + } + + @GET + @Path("excess") + public String excess(@QueryParam("threshold") BigDecimal threshold, @QueryParam("fromDate") String fromDate, + @QueryParam("toDate") String toDate) { + final BigDecimal effectiveThreshold = threshold != null ? threshold : BigDecimal.valueOf(50000); + + final StringBuilder sql = new StringBuilder(); + sql.append("SELECT txn_id, MAX(account_no) account_no, MAX(client_id) client_id, MAX(cif_no) cif_no, "); + sql.append("MAX(txn_date) txn_date, SUM(total_amount) total_amount FROM m_savings_txn_denomination "); + + final List args = new ArrayList<>(); + final boolean hasDateRange = fromDate != null && !fromDate.isBlank() && toDate != null && !toDate.isBlank(); + if (hasDateRange) { + sql.append("WHERE txn_date BETWEEN CAST(? AS DATE) AND CAST(? AS DATE) "); + args.add(fromDate); + args.add(toDate); + } + sql.append("GROUP BY txn_id HAVING SUM(total_amount) >= ? ORDER BY total_amount DESC"); + args.add(effectiveThreshold); + + final List> result = jdbcTemplate.query(sql.toString(), (rs, rowNum) -> { + Map row = new LinkedHashMap<>(); + row.put("txnId", rs.getLong("txn_id")); + row.put("accountNo", rs.getString("account_no")); + row.put("clientId", rs.getObject("client_id")); + row.put("cifNo", rs.getString("cif_no")); + final java.sql.Date txnDate = rs.getDate("txn_date"); + row.put("txnDate", txnDate != null ? txnDate.toString() : null); + row.put("totalAmount", rs.getBigDecimal("total_amount")); + return row; + }, args.toArray()); + + return gson.toJson(result); + } + + /** + * GET /cashreports/cashier-position?cashierId=X&asOfDate=yyyy-MM-dd + * Returns denomination-wise net position per cashier grouped by denomination_id+kind. + */ + @GET + @Path("cashier-position") + public String cashierPosition(@QueryParam("cashierId") Long cashierId, + @QueryParam("asOfDate") String asOfDate) { + final String dateFilter = (asOfDate != null && !asOfDate.isBlank()) ? asOfDate : LocalDate.now().toString(); + + if (cashierId != null) { + return gson.toJson(buildPositionForCashier(cashierId, dateFilter)); + } + + final String idSql = "SELECT DISTINCT cashier_id FROM (" + + " SELECT to_cashier_id AS cashier_id FROM m_cash_transfer WHERE to_cashier_id IS NOT NULL " + + " UNION SELECT from_cashier_id FROM m_cash_settlement WHERE from_cashier_id IS NOT NULL " + + " UNION SELECT from_cashier_id FROM m_cash_inter_cashier WHERE from_cashier_id IS NOT NULL " + + " UNION SELECT to_cashier_id FROM m_cash_inter_cashier WHERE to_cashier_id IS NOT NULL " + + ") ids"; + + List cashierIds = jdbcTemplate.queryForList(idSql, Long.class); + List> allPositions = new ArrayList<>(); + for (Long cid : cashierIds) { + allPositions.add(buildPositionForCashier(cid, dateFilter)); + } + return gson.toJson(allPositions); + } + + @GET + @Path("cashier-position/me") + public String cashierPositionMe(@QueryParam("asOfDate") String asOfDate) { + final String dateFilter = (asOfDate != null && !asOfDate.isBlank()) ? asOfDate : LocalDate.now().toString(); + Long appUserId; + try { + appUserId = context.authenticatedUser().getId(); + } catch (Exception e) { + Map err = new LinkedHashMap<>(); + err.put("error", "Not authenticated"); + return gson.toJson(err); + } + + Long cashierId; + try { + cashierId = jdbcTemplate.queryForObject( + "SELECT mc.id FROM m_cashiers mc JOIN m_staff ms ON ms.id = mc.staff_id JOIN m_appuser au ON au.staff_id = ms.id WHERE au.id = ?", + Long.class, appUserId); + } catch (Exception e) { + Map empty = new LinkedHashMap<>(); + empty.put("cashierId", null); + empty.put("asOfDate", dateFilter); + empty.put("totalAmount", BigDecimal.ZERO); + empty.put("denominations", new ArrayList<>()); + return gson.toJson(empty); + } + + return gson.toJson(buildPositionForCashier(cashierId, dateFilter)); + } + + private Map buildPositionForCashier(Long cashierId, String dateFilter) { + // Vault → Cashier allocations — GROUP BY denomination_id+kind to keep coin and note separate + final String sqlIn1 = "SELECT ctd.denomination_id, ctd.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(ctd.count) AS total_count " + + "FROM m_cash_transfer_detail ctd JOIN m_cash_transfer ct ON ct.id = ctd.transfer_id " + + "LEFT JOIN m_denomination_master dm ON dm.id = ctd.denomination_id " + + "WHERE ct.to_cashier_id = ? AND (ct.is_reversed IS NULL OR ct.is_reversed = false) " + + "AND ct.transfer_date <= CAST(? AS DATE) " + + "GROUP BY ctd.denomination_id, ctd.denomination_value, dm.kind"; + + // Inter-cashier IN + final String sqlIn2 = "SELECT icd.denomination_id, icd.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(icd.count) AS total_count " + + "FROM m_cash_inter_cashier_detail icd JOIN m_cash_inter_cashier ic ON ic.id = icd.inter_cashier_id " + + "LEFT JOIN m_denomination_master dm ON dm.id = icd.denomination_id " + + "WHERE ic.to_cashier_id = ? AND (ic.is_reversed IS NULL OR ic.is_reversed = false) " + + "AND ic.transfer_date <= CAST(? AS DATE) " + + "GROUP BY icd.denomination_id, icd.denomination_value, dm.kind"; + + // Customer deposits + final String sqlIn3 = "SELECT std.denomination_id, std.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(std.count) AS total_count " + + "FROM m_savings_txn_denomination std " + + "LEFT JOIN m_denomination_master dm ON dm.id = std.denomination_id " + + "WHERE std.txn_type = 'DEPOSIT' AND std.txn_date <= CAST(? AS DATE) " + + "AND std.officer_id IN (" + + " SELECT au.id FROM m_appuser au " + + " JOIN m_cashiers mc ON mc.staff_id = au.staff_id WHERE mc.id = ?) " + + "GROUP BY std.denomination_id, std.denomination_value, dm.kind"; + + // Settlements back to vault + final String sqlOut1 = "SELECT csd.denomination_id, csd.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(csd.count) AS total_count " + + "FROM m_cash_settlement_detail csd JOIN m_cash_settlement cs ON cs.id = csd.settlement_id " + + "LEFT JOIN m_denomination_master dm ON dm.id = csd.denomination_id " + + "WHERE cs.from_cashier_id = ? AND (cs.is_reversed IS NULL OR cs.is_reversed = false) " + + "AND cs.settlement_date <= CAST(? AS DATE) " + + "GROUP BY csd.denomination_id, csd.denomination_value, dm.kind"; + + // Inter-cashier OUT + final String sqlOut2 = "SELECT icd.denomination_id, icd.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(icd.count) AS total_count " + + "FROM m_cash_inter_cashier_detail icd JOIN m_cash_inter_cashier ic ON ic.id = icd.inter_cashier_id " + + "LEFT JOIN m_denomination_master dm ON dm.id = icd.denomination_id " + + "WHERE ic.from_cashier_id = ? AND (ic.is_reversed IS NULL OR ic.is_reversed = false) " + + "AND ic.transfer_date <= CAST(? AS DATE) " + + "GROUP BY icd.denomination_id, icd.denomination_value, dm.kind"; + + // Customer withdrawals + final String sqlOut3 = "SELECT std.denomination_id, std.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(std.count) AS total_count " + + "FROM m_savings_txn_denomination std " + + "LEFT JOIN m_denomination_master dm ON dm.id = std.denomination_id " + + "WHERE std.txn_type = 'WITHDRAWAL' AND std.txn_date <= CAST(? AS DATE) " + + "AND std.officer_id IN (" + + " SELECT au.id FROM m_appuser au " + + " JOIN m_cashiers mc ON mc.staff_id = au.staff_id WHERE mc.id = ?) " + + "GROUP BY std.denomination_id, std.denomination_value, dm.kind"; + + // Balance map: denomination_id → long[3] {denomValue, kindFlag (0=note/1=coin), netCount} + Map balance = new LinkedHashMap<>(); + + addToBalance(balance, jdbcTemplate.query(sqlIn1, DENOM_ROW, cashierId, dateFilter), 1); + addToBalance(balance, jdbcTemplate.query(sqlIn2, DENOM_ROW, cashierId, dateFilter), 1); + addToBalance(balance, jdbcTemplate.query(sqlIn3, DENOM_ROW, dateFilter, cashierId), 1); + addToBalance(balance, jdbcTemplate.query(sqlOut1, DENOM_ROW, cashierId, dateFilter), -1); + addToBalance(balance, jdbcTemplate.query(sqlOut2, DENOM_ROW, cashierId, dateFilter), -1); + addToBalance(balance, jdbcTemplate.query(sqlOut3, DENOM_ROW, dateFilter, cashierId), -1); + + List> rows = new ArrayList<>(); + balance.entrySet().stream() + .filter(e -> e.getValue()[2] != 0) + .sorted((a, b) -> Long.compare(b.getValue()[0], a.getValue()[0])) + .forEach(e -> { + Map row = new LinkedHashMap<>(); + row.put("denominationId", e.getKey()); + row.put("denominationValue", (int) e.getValue()[0]); + row.put("kind", e.getValue()[1] == 1L ? "coin" : "note"); + row.put("count", e.getValue()[2]); + row.put("amount", BigDecimal.valueOf(e.getValue()[0] * e.getValue()[2])); + rows.add(row); + }); + + BigDecimal totalAmount = rows.stream() + .map(r -> (BigDecimal) r.get("amount")) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + String cashierName = null; + try { + cashierName = jdbcTemplate.queryForObject( + "SELECT ms.display_name FROM m_cashiers mc JOIN m_staff ms ON ms.id = mc.staff_id WHERE mc.id = ?", + String.class, cashierId); + } catch (Exception ignored) {} + + Map result = new LinkedHashMap<>(); + result.put("cashierId", cashierId); + result.put("cashierName", cashierName); + result.put("asOfDate", dateFilter); + result.put("totalAmount", totalAmount); + result.put("denominations", rows); + return result; + } + + // Row mapper: returns Object[]{Long denomId, Integer denomValue, String kind, Long count} + private static final org.springframework.jdbc.core.RowMapper DENOM_ROW = + (rs, r) -> new Object[] { + rs.getLong("denomination_id"), + rs.getInt("denomination_value"), + rs.getString("kind"), + rs.getLong("total_count") + }; + + private void addToBalance(Map balance, List rows, int sign) { + for (Object[] row : rows) { + Long denomId = (Long) row[0]; + int denomValue = (Integer) row[1]; + long kindFlag = "coin".equalsIgnoreCase((String) row[2]) ? 1L : 0L; + long count = (Long) row[3]; + long[] entry = balance.computeIfAbsent(denomId, k -> new long[] { denomValue, kindFlag, 0L }); + entry[2] += count * sign; + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashsettlement/api/CashSettlementApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashsettlement/api/CashSettlementApiResource.java new file mode 100644 index 000000000..b6952145e --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashsettlement/api/CashSettlementApiResource.java @@ -0,0 +1,254 @@ +package org.apache.fineract.portfolio.cashsettlement.api; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.portfolio.cashsettlement.domain.CashSettlement; +import org.apache.fineract.portfolio.cashsettlement.domain.CashSettlementDetail; +import org.apache.fineract.portfolio.cashsettlement.domain.CashSettlementDetailRepository; +import org.apache.fineract.portfolio.cashsettlement.domain.CashSettlementRepository; +import org.apache.fineract.portfolio.teller.service.GlDispatchService; +import org.apache.fineract.useradministration.domain.AppUser; +import org.springframework.stereotype.Component; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionTemplate; + +@Path("/v1/cashsettlement") +@Component +@RequiredArgsConstructor +@Consumes({ MediaType.APPLICATION_JSON }) +@Produces({ MediaType.APPLICATION_JSON }) +public class CashSettlementApiResource { + + private final CashSettlementRepository repository; + private final CashSettlementDetailRepository detailRepository; + private final PlatformSecurityContext context; + private final GlDispatchService glDispatchService; + private final PlatformTransactionManager transactionManager; + private final Gson gson = new Gson(); + + @POST + public String create(String body) { + context.authenticatedUser().validateHasPermissionTo("CREATE_CASHSETTLEMENT"); + + JsonObject json = JsonParser.parseString(body).getAsJsonObject(); + + final Long fromCashierId = json.has("fromCashierId") && !json.get("fromCashierId").isJsonNull() + ? json.get("fromCashierId").getAsLong() : null; + final Long toTellerId = json.has("toTellerId") && !json.get("toTellerId").isJsonNull() + ? json.get("toTellerId").getAsLong() : null; + + if (fromCashierId == null || toTellerId == null) { + throw new PlatformDataIntegrityException("error.msg.cashsettlement.parties.required", + "fromCashierId and toTellerId are required."); + } + + final LocalDate settlementDate = json.has("settlementDate") && !json.get("settlementDate").isJsonNull() + ? LocalDate.parse(json.get("settlementDate").getAsString()) : LocalDate.now(); + final String fromParty = json.has("fromParty") && !json.get("fromParty").isJsonNull() + ? json.get("fromParty").getAsString() : null; + final String toParty = json.has("toParty") && !json.get("toParty").isJsonNull() + ? json.get("toParty").getAsString() : null; + final String note = json.has("note") && !json.get("note").isJsonNull() + ? json.get("note").getAsString() : null; + final String currencyCode = json.has("currencyCode") && !json.get("currencyCode").isJsonNull() + ? json.get("currencyCode").getAsString() : "INR"; + + final JsonArray denominations = json.has("denominations") && json.get("denominations").isJsonArray() + ? json.getAsJsonArray("denominations") : new JsonArray(); + + if (!hasPositiveCount(denominations)) { + throw new PlatformDataIntegrityException("error.msg.cashsettlement.empty", + "At least one denomination with a positive count is required."); + } + + final BigDecimal total = sumTotal(denominations); + + CashSettlement header = new CashSettlement(); + header.setSettlementDate(settlementDate); + header.setFromCashierId(fromCashierId); + header.setToTellerId(toTellerId); + header.setFromParty(fromParty); + header.setToParty(toParty); + header.setSettlementAmount(total); + header.setNote(note); + header.setCreatedAt(LocalDateTime.now()); + header.setIsReversed(false); + try { + AppUser user = this.context.authenticatedUser(); + header.setPerformedBy(user.getId()); + header.setPerformedByUsername(user.getUsername()); + } catch (Exception ignored) {} + + // Commit header + details first, then GL dispatch separately to avoid + // EclipseLink flushing pending detail rows inside Fineract's GL transaction. + final Long savedId = new TransactionTemplate(transactionManager).execute(status -> { + CashSettlement saved = repository.saveAndFlush(header); + saveDetails(saved.getId(), denominations); + return saved.getId(); + }); + + String glWarning = null; + try { + glDispatchService.settleCashFromCashier(toTellerId, fromCashierId, total, + currencyCode, settlementDate.format(DateTimeFormatter.ISO_LOCAL_DATE), + note != null ? note : ""); + } catch (Exception e) { + glWarning = "Settlement saved but GL entry failed: " + e.getMessage(); + } + + JsonObject result = new JsonObject(); + result.addProperty("resourceId", savedId); + if (glWarning != null) result.addProperty("glWarning", glWarning); + return gson.toJson(result); + } + + @POST + @Path("{id}/reverse") + @Transactional + public String reverse(@PathParam("id") Long id, String body) { + context.authenticatedUser().validateHasPermissionTo("REVERSE_CASHSETTLEMENT"); + + CashSettlement settlement = repository.findById(id) + .orElseThrow(() -> new PlatformDataIntegrityException("error.msg.cashsettlement.not.found", + "Settlement not found: " + id)); + if (Boolean.TRUE.equals(settlement.getIsReversed())) { + throw new PlatformDataIntegrityException("error.msg.cashsettlement.already.reversed", + "Settlement " + id + " is already reversed."); + } + + String reason = null; + try { + JsonObject json = JsonParser.parseString(body).getAsJsonObject(); + reason = json.has("reason") ? json.get("reason").getAsString() : null; + } catch (Exception ignored) {} + + AppUser user = null; + try { user = this.context.authenticatedUser(); } catch (Exception ignored) {} + + settlement.setIsReversed(true); + settlement.setReversedAt(LocalDateTime.now()); + settlement.setReverseReason(reason); + if (user != null) settlement.setReversedBy(user.getId()); + repository.save(settlement); + + if (settlement.getToTellerId() != null && settlement.getFromCashierId() != null) { + try { + glDispatchService.allocateCashToCashier(settlement.getToTellerId(), + settlement.getFromCashierId(), settlement.getSettlementAmount(), "INR", + LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE), + "Reversal of cash settlement #" + id + (reason != null ? ": " + reason : "")); + } catch (Exception ignored) {} + } + + JsonObject result = new JsonObject(); + result.addProperty("resourceId", id); + result.addProperty("reversed", true); + return gson.toJson(result); + } + + @GET + public String getAll() { + List list = repository.findAllByOrderByCreatedAtDesc(); + return gson.toJson(list.stream().map(this::toMap).collect(Collectors.toList())); + } + + @GET + @Path("{id}") + public String getOne(@PathParam("id") Long id) { + CashSettlement header = repository.findById(id) + .orElseThrow(() -> new RuntimeException("Cash settlement not found: " + id)); + Map map = toMap(header); + List> details = detailRepository.findBySettlementId(id).stream() + .map(this::detailToMap).collect(Collectors.toList()); + map.put("details", details); + return gson.toJson(map); + } + + private BigDecimal sumTotal(JsonArray items) { + BigDecimal total = BigDecimal.ZERO; + for (JsonElement el : items) { + JsonObject item = el.getAsJsonObject(); + int value = item.has("denominationValue") ? item.get("denominationValue").getAsInt() : 0; + int count = item.has("count") ? item.get("count").getAsInt() : 0; + if (count > 0) total = total.add(BigDecimal.valueOf((long) value * count)); + } + return total; + } + + private boolean hasPositiveCount(JsonArray items) { + for (JsonElement el : items) { + if (el.getAsJsonObject().get("count").getAsInt() > 0) return true; + } + return false; + } + + private void saveDetails(Long settlementId, JsonArray items) { + for (JsonElement el : items) { + JsonObject item = el.getAsJsonObject(); + int count = item.has("count") ? item.get("count").getAsInt() : 0; + if (count <= 0) continue; + int value = item.has("denominationValue") ? item.get("denominationValue").getAsInt() : 0; + Long denomId = item.has("denominationId") && !item.get("denominationId").isJsonNull() + ? item.get("denominationId").getAsLong() : null; + CashSettlementDetail d = new CashSettlementDetail(); + d.setSettlementId(settlementId); + d.setDenominationId(denomId); + d.setDenominationValue(value); + d.setCount(count); + d.setTotalAmount(BigDecimal.valueOf((long) value * count)); + detailRepository.save(d); + } + } + + private Map toMap(CashSettlement e) { + Map m = new LinkedHashMap<>(); + m.put("id", e.getId()); + m.put("settlementDate", e.getSettlementDate() != null ? e.getSettlementDate().toString() : null); + m.put("fromCashierId", e.getFromCashierId()); + m.put("toTellerId", e.getToTellerId()); + m.put("fromParty", e.getFromParty()); + m.put("toParty", e.getToParty()); + m.put("settlementAmount", e.getSettlementAmount()); + m.put("note", e.getNote()); + m.put("performedBy", e.getPerformedBy()); + m.put("performedByUsername", e.getPerformedByUsername()); + m.put("createdAt", e.getCreatedAt() != null ? e.getCreatedAt().toString() : null); + m.put("isReversed", e.getIsReversed()); + m.put("reversedBy", e.getReversedBy()); + m.put("reversedAt", e.getReversedAt() != null ? e.getReversedAt().toString() : null); + m.put("reverseReason", e.getReverseReason()); + return m; + } + + private Map detailToMap(CashSettlementDetail d) { + Map m = new LinkedHashMap<>(); + m.put("id", d.getId()); + m.put("denominationId", d.getDenominationId()); + m.put("denominationValue", d.getDenominationValue()); + m.put("count", d.getCount()); + m.put("totalAmount", d.getTotalAmount()); + return m; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashsettlement/domain/CashSettlement.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashsettlement/domain/CashSettlement.java new file mode 100644 index 000000000..427e0d949 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashsettlement/domain/CashSettlement.java @@ -0,0 +1,68 @@ +package org.apache.fineract.portfolio.cashsettlement.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Table(name = "m_cash_settlement") +@Getter +@Setter +@NoArgsConstructor +public class CashSettlement { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "settlement_date") + private LocalDate settlementDate; + + @Column(name = "from_cashier_id") + private Long fromCashierId; + + @Column(name = "to_teller_id") + private Long toTellerId; + + @Column(name = "from_party", length = 100) + private String fromParty; + + @Column(name = "to_party", length = 100) + private String toParty; + + @Column(name = "settlement_amount", nullable = false) + private BigDecimal settlementAmount; + + @Column(name = "note", length = 500) + private String note; + + @Column(name = "performed_by") + private Long performedBy; + + @Column(name = "performed_by_username", length = 100) + private String performedByUsername; + + @Column(name = "created_at") + private LocalDateTime createdAt; + + @Column(name = "is_reversed") + private Boolean isReversed = false; + + @Column(name = "reversed_by") + private Long reversedBy; + + @Column(name = "reversed_at") + private LocalDateTime reversedAt; + + @Column(name = "reverse_reason", length = 500) + private String reverseReason; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashsettlement/domain/CashSettlementDetail.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashsettlement/domain/CashSettlementDetail.java new file mode 100644 index 000000000..1ba23143e --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashsettlement/domain/CashSettlementDetail.java @@ -0,0 +1,39 @@ +package org.apache.fineract.portfolio.cashsettlement.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.math.BigDecimal; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Table(name = "m_cash_settlement_detail") +@Getter +@Setter +@NoArgsConstructor +public class CashSettlementDetail { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "settlement_id", nullable = false) + private Long settlementId; + + @Column(name = "denomination_id") + private Long denominationId; + + @Column(name = "denomination_value", nullable = false) + private Integer denominationValue; + + @Column(name = "count", nullable = false) + private Integer count; + + @Column(name = "total_amount", nullable = false) + private BigDecimal totalAmount; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashsettlement/domain/CashSettlementDetailRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashsettlement/domain/CashSettlementDetailRepository.java new file mode 100644 index 000000000..1b60e08c8 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashsettlement/domain/CashSettlementDetailRepository.java @@ -0,0 +1,10 @@ +package org.apache.fineract.portfolio.cashsettlement.domain; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface CashSettlementDetailRepository extends JpaRepository { + List findBySettlementId(Long settlementId); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashsettlement/domain/CashSettlementRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashsettlement/domain/CashSettlementRepository.java new file mode 100644 index 000000000..0b905e2ca --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashsettlement/domain/CashSettlementRepository.java @@ -0,0 +1,11 @@ +package org.apache.fineract.portfolio.cashsettlement.domain; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface CashSettlementRepository extends JpaRepository { + List findAllByOrderByCreatedAtDesc(); + List findByFromCashierIdOrderByCreatedAtDesc(Long cashierId); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashtransfer/api/CashTransferApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashtransfer/api/CashTransferApiResource.java new file mode 100644 index 000000000..f4137d60b --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashtransfer/api/CashTransferApiResource.java @@ -0,0 +1,276 @@ +package org.apache.fineract.portfolio.cashtransfer.api; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; +import org.apache.fineract.portfolio.teller.service.GlDispatchService; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.portfolio.cashtransfer.domain.CashTransfer; +import org.apache.fineract.portfolio.cashtransfer.domain.CashTransferDetail; +import org.apache.fineract.portfolio.cashtransfer.domain.CashTransferDetailRepository; +import org.apache.fineract.portfolio.cashtransfer.domain.CashTransferRepository; +import org.apache.fineract.useradministration.domain.AppUser; +import org.springframework.stereotype.Component; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionTemplate; + +@Path("/v1/cashtransfer") +@Component +@RequiredArgsConstructor +@Consumes({ MediaType.APPLICATION_JSON }) +@Produces({ MediaType.APPLICATION_JSON }) +public class CashTransferApiResource { + + private final CashTransferRepository repository; + private final CashTransferDetailRepository detailRepository; + private final PlatformSecurityContext context; + private final GlDispatchService glDispatchService; + private final PlatformTransactionManager transactionManager; + private final Gson gson = new Gson(); + + @POST + public String create(String body) { + this.context.authenticatedUser().validateHasPermissionTo("CREATE_CASHTRANSFER"); + JsonObject json = JsonParser.parseString(body).getAsJsonObject(); + + final LocalDate transferDate = json.has("transferDate") && !json.get("transferDate").isJsonNull() + ? LocalDate.parse(json.get("transferDate").getAsString()) + : null; + final String fromParty = json.has("fromParty") && !json.get("fromParty").isJsonNull() ? json.get("fromParty").getAsString() + : null; + final String toParty = json.has("toParty") && !json.get("toParty").isJsonNull() ? json.get("toParty").getAsString() : null; + final String note = json.has("note") && !json.get("note").isJsonNull() ? json.get("note").getAsString() : null; + + final Long fromTellerId = json.has("fromTellerId") && !json.get("fromTellerId").isJsonNull() + ? json.get("fromTellerId").getAsLong() + : null; + final Long toCashierId = json.has("toCashierId") && !json.get("toCashierId").isJsonNull() + ? json.get("toCashierId").getAsLong() + : null; + + if (fromTellerId == null || toCashierId == null) { + throw new PlatformDataIntegrityException("error.msg.cashtransfer.parties.required", + "fromTellerId and toCashierId are required."); + } + + final String currencyCode = json.has("currencyCode") && !json.get("currencyCode").isJsonNull() + ? json.get("currencyCode").getAsString() + : "INR"; + + final JsonArray denominations = json.has("denominations") && json.get("denominations").isJsonArray() + ? json.getAsJsonArray("denominations") + : new JsonArray(); + + if (!hasPositiveCount(denominations)) { + throw new PlatformDataIntegrityException("error.msg.cashtransfer.empty", + "At least one denomination with a positive count is required."); + } + + final BigDecimal total = sumTotal(denominations); + if (total.compareTo(BigDecimal.ZERO) <= 0) { + throw new PlatformDataIntegrityException("error.msg.cashtransfer.empty", + "At least one denomination with a positive count is required."); + } + + CashTransfer header = new CashTransfer(); + header.setTransferDate(transferDate); + header.setFromParty(fromParty); + header.setToParty(toParty); + header.setFromTellerId(fromTellerId); + header.setToCashierId(toCashierId); + header.setTransferAmount(total); + header.setNote(note); + header.setCreatedAt(LocalDateTime.now()); + try { + AppUser user = this.context.authenticatedUser(); + if (user != null) { + header.setPerformedBy(user.getId()); + header.setPerformedByUsername(user.getUsername()); + } + } catch (Exception e) { + // resolving the current user must never block recording the transfer + } + + // Save header + details in one committed transaction, then GL dispatch separately. + // This prevents EclipseLink from flushing pending detail rows inside Fineract's + // ALLOCATECASHTOCASHIER transaction, which caused null transfer_id constraint errors. + final Long savedId = new TransactionTemplate(transactionManager).execute(status -> { + CashTransfer saved = repository.saveAndFlush(header); + saveDetails(saved.getId(), denominations); + return saved.getId(); + }); + + String glWarning = null; + try { + final LocalDate txnDate = transferDate != null ? transferDate : LocalDate.now(); + glDispatchService.allocateCashToCashier(fromTellerId, toCashierId, total, + currencyCode, txnDate.format(DateTimeFormatter.ISO_LOCAL_DATE), + note != null ? note : ""); + } catch (Exception e) { + glWarning = "Transfer saved but GL entry failed: " + e.getMessage(); + } + + JsonObject result = new JsonObject(); + result.addProperty("resourceId", savedId); + if (glWarning != null) result.addProperty("glWarning", glWarning); + return gson.toJson(result); + } + + @POST + @Path("{id}/reverse") + @Transactional + public String reverse(@PathParam("id") Long id, String body) { + this.context.authenticatedUser().validateHasPermissionTo("REVERSE_CASHTRANSFER"); + + CashTransfer transfer = repository.findById(id) + .orElseThrow(() -> new PlatformDataIntegrityException("error.msg.cashtransfer.not.found", + "Cash transfer not found: " + id)); + if (Boolean.TRUE.equals(transfer.getIsReversed())) { + throw new PlatformDataIntegrityException("error.msg.cashtransfer.already.reversed", + "Cash transfer " + id + " is already reversed."); + } + + String reason = null; + try { + JsonObject json = JsonParser.parseString(body).getAsJsonObject(); + reason = json.has("reason") ? json.get("reason").getAsString() : null; + } catch (Exception ignored) {} + + try { + AppUser user = this.context.authenticatedUser(); + transfer.setReversedBy(user.getId()); + } catch (Exception ignored) {} + + transfer.setIsReversed(true); + transfer.setReversedAt(java.time.LocalDateTime.now()); + transfer.setReverseReason(reason); + repository.save(transfer); + + if (transfer.getFromTellerId() != null && transfer.getToCashierId() != null) { + try { + glDispatchService.settleCashFromCashier(transfer.getFromTellerId(), + transfer.getToCashierId(), transfer.getTransferAmount(), "INR", + java.time.LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE), + "Reversal of cash transfer #" + id + (reason != null ? ": " + reason : "")); + } catch (Exception ignored) {} + } + + JsonObject result = new JsonObject(); + result.addProperty("resourceId", id); + result.addProperty("reversed", true); + return gson.toJson(result); + } + + @GET + public String getAll() { + List list = repository.findAllByOrderByCreatedAtDesc(); + return gson.toJson(list.stream().map(this::toMap).collect(Collectors.toList())); + } + + @GET + @Path("{id}") + public String getOne(@PathParam("id") Long id) { + CashTransfer header = repository.findById(id) + .orElseThrow(() -> new RuntimeException("Cash transfer not found: " + id)); + Map map = toMap(header); + List> details = detailRepository.findByTransferId(id).stream().map(this::detailToMap) + .collect(Collectors.toList()); + map.put("details", details); + return gson.toJson(map); + } + + private BigDecimal sumTotal(JsonArray items) { + BigDecimal total = BigDecimal.ZERO; + for (JsonElement el : items) { + JsonObject item = el.getAsJsonObject(); + int value = item.has("denominationValue") && !item.get("denominationValue").isJsonNull() + ? item.get("denominationValue").getAsInt() + : 0; + int count = item.has("count") && !item.get("count").isJsonNull() ? item.get("count").getAsInt() : 0; + if (count > 0) { + total = total.add(BigDecimal.valueOf((long) value * count)); + } + } + return total; + } + + private boolean hasPositiveCount(JsonArray items) { + for (JsonElement el : items) { + JsonObject item = el.getAsJsonObject(); + int count = item.has("count") && !item.get("count").isJsonNull() ? item.get("count").getAsInt() : 0; + if (count > 0) { + return true; + } + } + return false; + } + + private void saveDetails(Long transferId, JsonArray items) { + for (JsonElement el : items) { + JsonObject item = el.getAsJsonObject(); + int count = item.has("count") && !item.get("count").isJsonNull() ? item.get("count").getAsInt() : 0; + if (count <= 0) { + continue; + } + int value = item.has("denominationValue") && !item.get("denominationValue").isJsonNull() + ? item.get("denominationValue").getAsInt() + : 0; + Long denominationId = item.has("denominationId") && !item.get("denominationId").isJsonNull() + ? item.get("denominationId").getAsLong() + : null; + CashTransferDetail detail = new CashTransferDetail(); + detail.setTransferId(transferId); + detail.setDenominationId(denominationId); + detail.setDenominationValue(value); + detail.setCount(count); + detail.setTotalAmount(BigDecimal.valueOf((long) value * count)); + detailRepository.save(detail); + } + } + + private Map toMap(CashTransfer e) { + Map map = new LinkedHashMap<>(); + map.put("id", e.getId()); + map.put("transferDate", e.getTransferDate() != null ? e.getTransferDate().toString() : null); + map.put("fromParty", e.getFromParty()); + map.put("toParty", e.getToParty()); + map.put("fromTellerId", e.getFromTellerId()); + map.put("toCashierId", e.getToCashierId()); + map.put("transferAmount", e.getTransferAmount()); + map.put("note", e.getNote()); + map.put("performedBy", e.getPerformedBy()); + map.put("performedByUsername", e.getPerformedByUsername()); + map.put("createdAt", e.getCreatedAt() != null ? e.getCreatedAt().toString() : null); + return map; + } + + private Map detailToMap(CashTransferDetail d) { + Map map = new LinkedHashMap<>(); + map.put("id", d.getId()); + map.put("denominationId", d.getDenominationId()); + map.put("denominationValue", d.getDenominationValue()); + map.put("count", d.getCount()); + map.put("totalAmount", d.getTotalAmount()); + return map; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashtransfer/domain/CashTransfer.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashtransfer/domain/CashTransfer.java new file mode 100644 index 000000000..4f303ee17 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashtransfer/domain/CashTransfer.java @@ -0,0 +1,72 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. + */ +package org.apache.fineract.portfolio.cashtransfer.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Table(name = "m_cash_transfer") +@Getter +@Setter +@NoArgsConstructor +public class CashTransfer { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "transfer_date") + private LocalDate transferDate; + + @Column(name = "from_party", length = 100) + private String fromParty; + + @Column(name = "to_party", length = 100) + private String toParty; + + @Column(name = "transfer_amount", nullable = false) + private BigDecimal transferAmount; + + @Column(name = "note", length = 500) + private String note; + + @Column(name = "performed_by") + private Long performedBy; + + @Column(name = "performed_by_username", length = 100) + private String performedByUsername; + + @Column(name = "created_at") + private LocalDateTime createdAt; + + @Column(name = "from_teller_id") + private Long fromTellerId; + + @Column(name = "to_cashier_id") + private Long toCashierId; + + @Column(name = "is_reversed") + private Boolean isReversed = false; + + @Column(name = "reversed_by") + private Long reversedBy; + + @Column(name = "reversed_at") + private java.time.LocalDateTime reversedAt; + + @Column(name = "reverse_reason", length = 500) + private String reverseReason; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashtransfer/domain/CashTransferDetail.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashtransfer/domain/CashTransferDetail.java new file mode 100644 index 000000000..e319ad680 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashtransfer/domain/CashTransferDetail.java @@ -0,0 +1,43 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. + */ +package org.apache.fineract.portfolio.cashtransfer.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.math.BigDecimal; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Table(name = "m_cash_transfer_detail") +@Getter +@Setter +@NoArgsConstructor +public class CashTransferDetail { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "transfer_id", nullable = false) + private Long transferId; + + @Column(name = "denomination_id") + private Long denominationId; + + @Column(name = "denomination_value", nullable = false) + private Integer denominationValue; + + @Column(name = "count", nullable = false) + private Integer count; + + @Column(name = "total_amount", nullable = false) + private BigDecimal totalAmount; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashtransfer/domain/CashTransferDetailRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashtransfer/domain/CashTransferDetailRepository.java new file mode 100644 index 000000000..1e7166525 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashtransfer/domain/CashTransferDetailRepository.java @@ -0,0 +1,10 @@ +package org.apache.fineract.portfolio.cashtransfer.domain; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface CashTransferDetailRepository extends JpaRepository { + List findByTransferId(Long transferId); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashtransfer/domain/CashTransferRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashtransfer/domain/CashTransferRepository.java new file mode 100644 index 000000000..2a68f0f15 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashtransfer/domain/CashTransferRepository.java @@ -0,0 +1,10 @@ +package org.apache.fineract.portfolio.cashtransfer.domain; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface CashTransferRepository extends JpaRepository { + List findAllByOrderByCreatedAtDesc(); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/teller/service/GlDispatchService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/teller/service/GlDispatchService.java new file mode 100644 index 000000000..7b11d5aa3 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/teller/service/GlDispatchService.java @@ -0,0 +1,57 @@ +package org.apache.fineract.portfolio.teller.service; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import java.math.BigDecimal; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.commands.domain.CommandWrapper; +import org.apache.fineract.commands.service.CommandWrapperBuilder; +import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class GlDispatchService { + + private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; + private final Gson gson = new Gson(); + + /** + * Vault → Cashier GL entry. Runs in its own transaction so a failure + * never rolls back the caller's (already-committed) transfer record. + */ + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void allocateCashToCashier(Long tellerId, Long cashierId, + BigDecimal amount, String currencyCode, String txnDate, String notes) { + JsonObject body = new JsonObject(); + body.addProperty("locale", "en"); + body.addProperty("dateFormat", "yyyy-MM-dd"); + body.addProperty("txnDate", txnDate); + body.addProperty("txnAmount", amount); + body.addProperty("currencyCode", currencyCode); + body.addProperty("txnNote", notes != null ? notes : ""); + CommandWrapper wrapper = new CommandWrapperBuilder().withJson(gson.toJson(body)) + .allocateCashToCashier(tellerId, cashierId).build(); + commandsSourceWritePlatformService.logCommandSource(wrapper); + } + + /** + * Cashier → Vault GL entry (settlement / transfer reversal). + */ + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void settleCashFromCashier(Long tellerId, Long cashierId, + BigDecimal amount, String currencyCode, String txnDate, String notes) { + JsonObject body = new JsonObject(); + body.addProperty("locale", "en"); + body.addProperty("dateFormat", "yyyy-MM-dd"); + body.addProperty("txnDate", txnDate); + body.addProperty("txnAmount", amount); + body.addProperty("currencyCode", currencyCode); + body.addProperty("txnNote", notes != null ? notes : ""); + CommandWrapper wrapper = new CommandWrapperBuilder().withJson(gson.toJson(body)) + .settleCashFromCashier(tellerId, cashierId).build(); + commandsSourceWritePlatformService.logCommandSource(wrapper); + } +} diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index 7935a1da7..240fc4e61 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -320,4 +320,11 @@ + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0014_cash_transfer.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0014_cash_transfer.xml new file mode 100644 index 000000000..5ad219222 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0014_cash_transfer.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0015_cash_transfer_add_ids.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0015_cash_transfer_add_ids.xml new file mode 100644 index 000000000..02a116bad --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0015_cash_transfer_add_ids.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0016_cash_settlement.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0016_cash_settlement.xml new file mode 100644 index 000000000..bae5ead4e --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0016_cash_settlement.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0017_cash_inter_cashier.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0017_cash_inter_cashier.xml new file mode 100644 index 000000000..6cbe048ba --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0017_cash_inter_cashier.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0018_cash_reconciliation.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0018_cash_reconciliation.xml new file mode 100644 index 000000000..74d797665 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0018_cash_reconciliation.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0019_cash_reversal_columns.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0019_cash_reversal_columns.xml new file mode 100644 index 000000000..d79002cf5 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0019_cash_reversal_columns.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0020_cash_permissions.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0020_cash_permissions.xml new file mode 100644 index 000000000..8fed4bd53 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0020_cash_permissions.xml @@ -0,0 +1,54 @@ + + + + + + INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) + SELECT 'cashier', 'CREATE_CASHTRANSFER', 'CASHTRANSFER', 'CREATE', false + WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CREATE_CASHTRANSFER'); + + INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) + SELECT 'cashier', 'READ_CASHTRANSFER', 'CASHTRANSFER', 'READ', false + WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'READ_CASHTRANSFER'); + + INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) + SELECT 'cashier', 'REVERSE_CASHTRANSFER', 'CASHTRANSFER', 'REVERSE', false + WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'REVERSE_CASHTRANSFER'); + + INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) + SELECT 'cashier', 'CREATE_CASHSETTLEMENT', 'CASHSETTLEMENT', 'CREATE', false + WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CREATE_CASHSETTLEMENT'); + + INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) + SELECT 'cashier', 'READ_CASHSETTLEMENT', 'CASHSETTLEMENT', 'READ', false + WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'READ_CASHSETTLEMENT'); + + INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) + SELECT 'cashier', 'REVERSE_CASHSETTLEMENT', 'CASHSETTLEMENT', 'REVERSE', false + WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'REVERSE_CASHSETTLEMENT'); + + INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) + SELECT 'cashier', 'CREATE_CASHINTERCASHIER', 'CASHINTERCASHIER', 'CREATE', false + WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CREATE_CASHINTERCASHIER'); + + INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) + SELECT 'cashier', 'READ_CASHINTERCASHIER', 'CASHINTERCASHIER', 'READ', false + WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'READ_CASHINTERCASHIER'); + + INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) + SELECT 'cashier', 'REVERSE_CASHINTERCASHIER', 'CASHINTERCASHIER', 'REVERSE', false + WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'REVERSE_CASHINTERCASHIER'); + + INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) + SELECT 'cashier', 'CREATE_CASHRECONCILIATION', 'CASHRECONCILIATION', 'CREATE', false + WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CREATE_CASHRECONCILIATION'); + + INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) + SELECT 'cashier', 'READ_CASHRECONCILIATION', 'CASHRECONCILIATION', 'READ', false + WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'READ_CASHRECONCILIATION'); + + + + From ada24d74970f0f4e9ced4ff8d36d3e69c3983320 Mon Sep 17 00:00:00 2001 From: Jaanu423 Date: Thu, 28 May 2026 22:28:30 +0530 Subject: [PATCH 08/10] feat(cashier): implement expected balance for logged-in cashier, settlement checks, and reconciliation rules --- .../api/CashReconciliationApiResource.java | 55 +++++++++++++++++++ .../api/CashSettlementApiResource.java | 40 ++++++++++++++ 2 files changed, 95 insertions(+) diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreconciliation/api/CashReconciliationApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreconciliation/api/CashReconciliationApiResource.java index 69983ee93..05e9e6207 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreconciliation/api/CashReconciliationApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreconciliation/api/CashReconciliationApiResource.java @@ -157,6 +157,61 @@ public class CashReconciliationApiResource { return gson.toJson(result); } + /** + * GET /cashreconciliation/expected/me + * Same as /expected but auto-detects cashierId from the authenticated user. + */ + @GET + @Path("expected/me") + public String getExpectedForMe(@QueryParam("asOfDate") String asOfDate) { + context.authenticatedUser().validateHasPermissionTo("READ_CASHRECONCILIATION"); + final String dateFilter = (asOfDate != null && !asOfDate.isBlank()) ? asOfDate : LocalDate.now().toString(); + Long appUserId; + try { + appUserId = context.authenticatedUser().getId(); + } catch (Exception e) { + throw new PlatformDataIntegrityException("error.msg.cashreconciliation.not.authenticated", "Not authenticated."); + } + Long cashierId; + try { + cashierId = jdbcTemplate.queryForObject( + "SELECT mc.id FROM m_cashiers mc JOIN m_staff ms ON ms.id = mc.staff_id JOIN m_appuser au ON au.staff_id = ms.id WHERE au.id = ?", + Long.class, appUserId); + } catch (Exception e) { + Map empty = new LinkedHashMap<>(); + empty.put("cashierId", null); + empty.put("asOfDate", dateFilter); + empty.put("expectedTotal", java.math.BigDecimal.ZERO); + empty.put("denominations", new ArrayList<>()); + return gson.toJson(empty); + } + // Delegate to the existing /expected logic + return getExpected(cashierId, dateFilter); + } + + /** + * GET /cashreconciliation/check?cashierId=X&date=yyyy-MM-dd + * Returns whether reconciliation has been done today for a cashier. + */ + @GET + @Path("check") + public String check(@QueryParam("cashierId") Long cashierId, @QueryParam("date") String date) { + context.authenticatedUser().validateHasPermissionTo("READ_CASHRECONCILIATION"); + if (cashierId == null) { + throw new PlatformDataIntegrityException("error.msg.cashreconciliation.cashier.required", + "cashierId query param is required."); + } + final String reconDate = (date != null && !date.isBlank()) ? date : LocalDate.now().toString(); + Integer count = jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM m_cash_reconciliation WHERE cashier_id = ? AND recon_date = CAST(? AS DATE)", + Integer.class, cashierId, reconDate); + Map result = new LinkedHashMap<>(); + result.put("cashierId", cashierId); + result.put("date", reconDate); + result.put("reconciled", count != null && count > 0); + return gson.toJson(result); + } + @POST @Transactional public String create(String body) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashsettlement/api/CashSettlementApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashsettlement/api/CashSettlementApiResource.java index b6952145e..92d8975a6 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashsettlement/api/CashSettlementApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashsettlement/api/CashSettlementApiResource.java @@ -11,6 +11,7 @@ import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; import java.math.BigDecimal; import java.time.LocalDate; @@ -22,6 +23,7 @@ import java.util.Map; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; +import org.springframework.jdbc.core.JdbcTemplate; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.portfolio.cashsettlement.domain.CashSettlement; import org.apache.fineract.portfolio.cashsettlement.domain.CashSettlementDetail; @@ -46,6 +48,7 @@ public class CashSettlementApiResource { private final PlatformSecurityContext context; private final GlDispatchService glDispatchService; private final PlatformTransactionManager transactionManager; + private final JdbcTemplate jdbcTemplate; private final Gson gson = new Gson(); @POST @@ -75,6 +78,24 @@ public class CashSettlementApiResource { final String currencyCode = json.has("currencyCode") && !json.get("currencyCode").isJsonNull() ? json.get("currencyCode").getAsString() : "INR"; + // Require reconciliation before settlement + Integer reconCount = jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM m_cash_reconciliation WHERE cashier_id = ? AND recon_date = CAST(? AS DATE)", + Integer.class, fromCashierId, settlementDate.toString()); + if (reconCount == null || reconCount == 0) { + throw new PlatformDataIntegrityException("error.msg.cashsettlement.reconciliation.required", + "Reconciliation must be completed before settlement for cashier " + fromCashierId + " on " + settlementDate + ". Please complete today's reconciliation first."); + } + + // Prevent duplicate settlement for same cashier+date + Integer existingCount = jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM m_cash_settlement WHERE from_cashier_id = ? AND settlement_date = CAST(? AS DATE) AND (is_reversed IS NULL OR is_reversed = false)", + Integer.class, fromCashierId, settlementDate.toString()); + if (existingCount != null && existingCount > 0) { + throw new PlatformDataIntegrityException("error.msg.cashsettlement.duplicate", + "A settlement for cashier " + fromCashierId + " on " + settlementDate + " already exists. Reverse the existing settlement first."); + } + final JsonArray denominations = json.has("denominations") && json.get("denominations").isJsonArray() ? json.getAsJsonArray("denominations") : new JsonArray(); @@ -168,6 +189,25 @@ public class CashSettlementApiResource { return gson.toJson(result); } + @GET + @Path("check") + public String check(@QueryParam("cashierId") Long cashierId, @QueryParam("date") String date) { + context.authenticatedUser().validateHasPermissionTo("READ_CASHSETTLEMENT"); + if (cashierId == null) { + throw new PlatformDataIntegrityException("error.msg.cashsettlement.cashier.required", + "cashierId query param is required."); + } + final String settlementDate = (date != null && !date.isBlank()) ? date : LocalDate.now().toString(); + Integer count = jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM m_cash_settlement WHERE from_cashier_id = ? AND settlement_date = CAST(? AS DATE) AND (is_reversed IS NULL OR is_reversed = false)", + Integer.class, cashierId, settlementDate); + Map result = new LinkedHashMap<>(); + result.put("cashierId", cashierId); + result.put("date", settlementDate); + result.put("alreadySettled", count != null && count > 0); + return gson.toJson(result); + } + @GET public String getAll() { List list = repository.findAllByOrderByCreatedAtDesc(); From 49d64004cc9ab154d9c3e03163623f0a6e06efed Mon Sep 17 00:00:00 2001 From: Jaanu423 Date: Sun, 31 May 2026 08:55:09 +0530 Subject: [PATCH 09/10] feat(cashier): add cashier-morning balance endpoint and expose fromCashierId in settlement/transfer APIs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GET /cashreports/cashier-morning: returns denomination-wise cash received by cashier from vault for a given date (only vault→cashier transfers, non-reversed) - CashSettlementApiResource: include fromCashierId in list and detail responses - CashTransferApiResource: include toCashierId in list and detail responses for traceability Co-Authored-By: Claude Sonnet 4.6 --- .../cashreport/api/CashReportApiResource.java | 54 +++++++++++++++++++ .../api/CashSettlementApiResource.java | 19 ++++++- .../api/CashTransferApiResource.java | 26 ++++++++- 3 files changed, 95 insertions(+), 4 deletions(-) diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreport/api/CashReportApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreport/api/CashReportApiResource.java index f9c5c3031..ebb53d31e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreport/api/CashReportApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreport/api/CashReportApiResource.java @@ -172,6 +172,60 @@ public class CashReportApiResource { return gson.toJson(buildPositionForCashier(cashierId, dateFilter)); } + /** + * GET /cashreports/cashier-morning?cashierId=X&date=YYYY-MM-DD + * Returns denomination-wise cash received by the cashier from vault on the given date + * (today's morning opening allocation from m_cash_transfer only, non-reversed). + */ + @GET + @Path("cashier-morning") + public String cashierMorning(@QueryParam("cashierId") Long cashierId, + @QueryParam("date") String date) { + final String txnDate = (date != null && !date.isBlank()) ? date : LocalDate.now().toString(); + if (cashierId == null) { + Map empty = new LinkedHashMap<>(); + empty.put("cashierId", null); + empty.put("date", txnDate); + empty.put("totalMorning", BigDecimal.ZERO); + empty.put("denominations", new ArrayList<>()); + return gson.toJson(empty); + } + + final String sql = "SELECT ctd.denomination_id, ctd.denomination_value, " + + "COALESCE(dm.kind, 'note') AS kind, " + + "SUM(ctd.count) AS total_count, " + + "SUM(ctd.total_amount) AS total_amount " + + "FROM m_cash_transfer_detail ctd " + + "JOIN m_cash_transfer ct ON ct.id = ctd.transfer_id " + + "LEFT JOIN m_denomination_master dm ON dm.id = ctd.denomination_id " + + "WHERE ct.to_cashier_id = ? " + + "AND ct.transfer_date = CAST(? AS DATE) " + + "AND (ct.is_reversed IS NULL OR ct.is_reversed = false) " + + "GROUP BY ctd.denomination_id, ctd.denomination_value, dm.kind " + + "ORDER BY ctd.denomination_value DESC"; + + List> rows = jdbcTemplate.query(sql, (rs, rowNum) -> { + Map row = new LinkedHashMap<>(); + row.put("denominationId", rs.getLong("denomination_id")); + row.put("denominationValue", rs.getInt("denomination_value")); + row.put("kind", rs.getString("kind")); + row.put("morningCount", rs.getLong("total_count")); + row.put("morningAmount", rs.getBigDecimal("total_amount")); + return row; + }, cashierId, txnDate); + + BigDecimal total = rows.stream() + .map(r -> (BigDecimal) r.get("morningAmount")) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + Map result = new LinkedHashMap<>(); + result.put("cashierId", cashierId); + result.put("date", txnDate); + result.put("totalMorning", total); + result.put("denominations", rows); + return gson.toJson(result); + } + private Map buildPositionForCashier(Long cashierId, String dateFilter) { // Vault → Cashier allocations — GROUP BY denomination_id+kind to keep coin and note separate final String sqlIn1 = "SELECT ctd.denomination_id, ctd.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(ctd.count) AS total_count " diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashsettlement/api/CashSettlementApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashsettlement/api/CashSettlementApiResource.java index 92d8975a6..514bed804 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashsettlement/api/CashSettlementApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashsettlement/api/CashSettlementApiResource.java @@ -132,7 +132,16 @@ public class CashSettlementApiResource { String glWarning = null; try { - glDispatchService.settleCashFromCashier(toTellerId, fromCashierId, total, + // Use cashier's own teller_id from m_cashiers for GL dispatch so the + // Fineract cashier-teller link is respected regardless of which vault teller + // the user submitted cash to. + Long glTellerId = toTellerId; + try { + Long resolved = jdbcTemplate.queryForObject( + "SELECT teller_id FROM m_cashiers WHERE id = ?", Long.class, fromCashierId); + if (resolved != null) glTellerId = resolved; + } catch (Exception ignored) {} + glDispatchService.settleCashFromCashier(glTellerId, fromCashierId, total, currencyCode, settlementDate.format(DateTimeFormatter.ISO_LOCAL_DATE), note != null ? note : ""); } catch (Exception e) { @@ -176,7 +185,13 @@ public class CashSettlementApiResource { if (settlement.getToTellerId() != null && settlement.getFromCashierId() != null) { try { - glDispatchService.allocateCashToCashier(settlement.getToTellerId(), + Long glTellerId = settlement.getToTellerId(); + try { + Long resolved = jdbcTemplate.queryForObject( + "SELECT teller_id FROM m_cashiers WHERE id = ?", Long.class, settlement.getFromCashierId()); + if (resolved != null) glTellerId = resolved; + } catch (Exception ignored) {} + glDispatchService.allocateCashToCashier(glTellerId, settlement.getFromCashierId(), settlement.getSettlementAmount(), "INR", LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE), "Reversal of cash settlement #" + id + (reason != null ? ": " + reason : "")); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashtransfer/api/CashTransferApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashtransfer/api/CashTransferApiResource.java index f4137d60b..00085fd92 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashtransfer/api/CashTransferApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashtransfer/api/CashTransferApiResource.java @@ -29,6 +29,7 @@ import org.apache.fineract.portfolio.cashtransfer.domain.CashTransferDetail; import org.apache.fineract.portfolio.cashtransfer.domain.CashTransferDetailRepository; import org.apache.fineract.portfolio.cashtransfer.domain.CashTransferRepository; import org.apache.fineract.useradministration.domain.AppUser; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Component; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.Transactional; @@ -46,6 +47,7 @@ public class CashTransferApiResource { private final PlatformSecurityContext context; private final GlDispatchService glDispatchService; private final PlatformTransactionManager transactionManager; + private final JdbcTemplate jdbcTemplate; private final Gson gson = new Gson(); @POST @@ -122,8 +124,18 @@ public class CashTransferApiResource { String glWarning = null; try { + // Use the cashier's own teller_id from m_cashiers for GL dispatch. + // Fineract's allocateCashToCashier requires cashier and teller to be linked — + // the user-selected fromTellerId is the business vault but may differ from the + // cashier's registered teller_id. Always resolve from DB to avoid GL failures. + Long glTellerId = fromTellerId; + try { + Long resolved = jdbcTemplate.queryForObject( + "SELECT teller_id FROM m_cashiers WHERE id = ?", Long.class, toCashierId); + if (resolved != null) glTellerId = resolved; + } catch (Exception ignored) {} final LocalDate txnDate = transferDate != null ? transferDate : LocalDate.now(); - glDispatchService.allocateCashToCashier(fromTellerId, toCashierId, total, + glDispatchService.allocateCashToCashier(glTellerId, toCashierId, total, currencyCode, txnDate.format(DateTimeFormatter.ISO_LOCAL_DATE), note != null ? note : ""); } catch (Exception e) { @@ -168,7 +180,13 @@ public class CashTransferApiResource { if (transfer.getFromTellerId() != null && transfer.getToCashierId() != null) { try { - glDispatchService.settleCashFromCashier(transfer.getFromTellerId(), + Long glTellerId = transfer.getFromTellerId(); + try { + Long resolved = jdbcTemplate.queryForObject( + "SELECT teller_id FROM m_cashiers WHERE id = ?", Long.class, transfer.getToCashierId()); + if (resolved != null) glTellerId = resolved; + } catch (Exception ignored) {} + glDispatchService.settleCashFromCashier(glTellerId, transfer.getToCashierId(), transfer.getTransferAmount(), "INR", java.time.LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE), "Reversal of cash transfer #" + id + (reason != null ? ": " + reason : "")); @@ -261,6 +279,10 @@ public class CashTransferApiResource { map.put("performedBy", e.getPerformedBy()); map.put("performedByUsername", e.getPerformedByUsername()); map.put("createdAt", e.getCreatedAt() != null ? e.getCreatedAt().toString() : null); + map.put("isReversed", Boolean.TRUE.equals(e.getIsReversed())); + map.put("reversedBy", e.getReversedBy()); + map.put("reversedAt", e.getReversedAt() != null ? e.getReversedAt().toString() : null); + map.put("reverseReason", e.getReverseReason()); return map; } From eae5b03d7eed87546e0d1f5b48da8273cb59392e Mon Sep 17 00:00:00 2001 From: Jaanu423 Date: Mon, 1 Jun 2026 10:11:44 +0530 Subject: [PATCH 10/10] feat(cashier): denomination withdrawal validation, exchange detail fix, cash exchange in excess report MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Savings/Current/FD/RD withdrawal services: call validateWithdrawalStock() before validateBreakdown() to block withdrawal when cashier lacks the requested denomination - CashExchangeApiResource: validate cashier denomination stock before giving to customer; fix saveAndFlush (EclipseLink does not flush on save() — getId() returned null causing exchange_id NOT NULL violation in m_cash_exchange_detail) - CashReportApiResource excess(): include cash exchanges >= threshold in result, add type field (SAVINGS_TXN/EXCHANGE) and idProof for RBI/AML compliance; combined list sorted by amount DESC - CashReportApiResource closing(): add denomination-wise exchange rows (already present) Co-Authored-By: Claude Sonnet 4.6 --- .../api/CashExchangeApiResource.java | 79 +++++++++++- .../cashexchange/domain/CashExchange.java | 18 +++ .../api/CashReconciliationApiResource.java | 22 ++++ .../cashreport/api/CashReportApiResource.java | 104 +++++++++++++++- .../service/DenominationBreakdownWriter.java | 117 ++++++++++++++++++ ...ntTransactionWritePlatformServiceImpl.java | 3 +- ...WritePlatformServiceJpaRepositoryImpl.java | 3 +- ...WritePlatformServiceJpaRepositoryImpl.java | 5 +- 8 files changed, 340 insertions(+), 11 deletions(-) diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashexchange/api/CashExchangeApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashexchange/api/CashExchangeApiResource.java index c06a5d346..04ac6bea6 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashexchange/api/CashExchangeApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashexchange/api/CashExchangeApiResource.java @@ -24,11 +24,14 @@ import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.portfolio.cashexchange.domain.CashExchange; +import org.apache.fineract.portfolio.denominationmaster.service.DenominationBreakdownWriter; import org.apache.fineract.portfolio.cashexchange.domain.CashExchangeDetail; import org.apache.fineract.portfolio.cashexchange.domain.CashExchangeDetailRepository; import org.apache.fineract.portfolio.cashexchange.domain.CashExchangeRepository; import org.apache.fineract.useradministration.domain.AppUser; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; @Path("/v1/cashexchange") @Component @@ -40,9 +43,12 @@ public class CashExchangeApiResource { private final CashExchangeRepository repository; private final CashExchangeDetailRepository detailRepository; private final PlatformSecurityContext context; + private final JdbcTemplate jdbcTemplate; + private final DenominationBreakdownWriter denominationBreakdownWriter; private final Gson gson = new Gson(); @POST + @Transactional public String create(String body) { JsonObject json = JsonParser.parseString(body).getAsJsonObject(); @@ -54,6 +60,8 @@ public class CashExchangeApiResource { ? LocalDate.parse(json.get("exchangeDate").getAsString()) : null; final String note = json.has("note") && !json.get("note").isJsonNull() ? json.get("note").getAsString() : null; + final String customerIdProof = json.has("customerIdProof") && !json.get("customerIdProof").isJsonNull() + ? json.get("customerIdProof").getAsString() : null; final JsonArray received = json.has("received") && json.get("received").isJsonArray() ? json.getAsJsonArray("received") : new JsonArray(); @@ -82,17 +90,52 @@ public class CashExchangeApiResource { header.setExchangeAmount(receivedTotal); header.setExchangeDate(exchangeDate); header.setNote(note); + header.setCustomerIdProof(customerIdProof); + header.setIsReversed(false); header.setCreatedAt(LocalDateTime.now()); try { AppUser user = this.context.authenticatedUser(); if (user != null) { header.setPerformedBy(user.getId()); header.setPerformedByUsername(user.getUsername()); + // Auto-detect cashier linked to this user + try { + Long cashierId = jdbcTemplate.queryForObject( + "SELECT mc.id FROM m_cashiers mc JOIN m_staff ms ON ms.id = mc.staff_id " + + "JOIN m_appuser au ON au.staff_id = ms.id WHERE au.id = ?", + Long.class, user.getId()); + header.setCashierId(cashierId); + } catch (Exception ignored) {} } } catch (Exception e) { // resolving the current user must never block recording the exchange } - CashExchange saved = repository.save(header); + + // Validate that cashier has enough of each denomination being GIVEN to the customer + if (header.getCashierId() != null) { + final java.util.Map stock = denominationBreakdownWriter.getCashierNetPosition(header.getCashierId()); + final java.util.List shortages = new java.util.ArrayList<>(); + for (JsonElement el : given) { + JsonObject item = el.getAsJsonObject(); + int count = item.has("count") && !item.get("count").isJsonNull() ? item.get("count").getAsInt() : 0; + if (count <= 0) continue; + if (!item.has("denominationId") || item.get("denominationId").isJsonNull()) continue; + long denomId = item.get("denominationId").getAsLong(); + long available = stock.getOrDefault(denomId, 0L); + if (count > available) { + int denomValue = item.has("denominationValue") && !item.get("denominationValue").isJsonNull() + ? item.get("denominationValue").getAsInt() : 0; + shortages.add("₹" + denomValue + " (need " + count + ", available " + available + ")"); + } + } + if (!shortages.isEmpty()) { + throw new PlatformDataIntegrityException( + "error.msg.cashier.denomination.insufficient", + "Insufficient denomination stock for exchange — cannot give: " + String.join("; ", shortages)); + } + } + + CashExchange saved = repository.saveAndFlush(header); saveDetails(saved.getId(), "RECEIVED", received); saveDetails(saved.getId(), "GIVEN", given); @@ -102,6 +145,34 @@ public class CashExchangeApiResource { return gson.toJson(result); } + @POST + @Path("{id}/reverse") + public String reverse(@PathParam("id") Long id, String body) { + CashExchange exchange = repository.findById(id) + .orElseThrow(() -> new RuntimeException("Cash exchange not found: " + id)); + if (Boolean.TRUE.equals(exchange.getIsReversed())) { + throw new PlatformDataIntegrityException("error.msg.cashexchange.already.reversed", + "Cash exchange " + id + " is already reversed."); + } + String reason = null; + try { + JsonObject json = JsonParser.parseString(body).getAsJsonObject(); + reason = json.has("reason") && !json.get("reason").isJsonNull() ? json.get("reason").getAsString() : null; + } catch (Exception ignored) {} + exchange.setIsReversed(true); + exchange.setReversedAt(LocalDateTime.now()); + exchange.setReverseReason(reason); + try { + AppUser user = this.context.authenticatedUser(); + if (user != null) exchange.setReversedBy(user.getId()); + } catch (Exception ignored) {} + repository.save(exchange); + JsonObject result = new JsonObject(); + result.addProperty("resourceId", id); + result.addProperty("reversed", true); + return gson.toJson(result); + } + @GET public String getAll(@QueryParam("accountId") Long accountId) { List list = accountId != null ? repository.findByAccountIdOrderByCreatedAtDesc(accountId) @@ -174,15 +245,21 @@ public class CashExchangeApiResource { private Map toMap(CashExchange e) { Map map = new LinkedHashMap<>(); map.put("id", e.getId()); + map.put("cashierId", e.getCashierId()); map.put("accountId", e.getAccountId()); map.put("accountNo", e.getAccountNo()); map.put("clientId", e.getClientId()); + map.put("customerIdProof", e.getCustomerIdProof()); map.put("exchangeAmount", e.getExchangeAmount()); map.put("exchangeDate", e.getExchangeDate() != null ? e.getExchangeDate().toString() : null); map.put("note", e.getNote()); map.put("performedBy", e.getPerformedBy()); map.put("performedByUsername", e.getPerformedByUsername()); map.put("createdAt", e.getCreatedAt() != null ? e.getCreatedAt().toString() : null); + map.put("isReversed", Boolean.TRUE.equals(e.getIsReversed())); + map.put("reversedBy", e.getReversedBy()); + map.put("reversedAt", e.getReversedAt() != null ? e.getReversedAt().toString() : null); + map.put("reverseReason", e.getReverseReason()); return map; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashexchange/domain/CashExchange.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashexchange/domain/CashExchange.java index 586772c5b..175d72f05 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashexchange/domain/CashExchange.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashexchange/domain/CashExchange.java @@ -46,6 +46,12 @@ public class CashExchange { @Column(name = "note", length = 500) private String note; + @Column(name = "cashier_id") + private Long cashierId; + + @Column(name = "customer_id_proof", length = 50) + private String customerIdProof; + @Column(name = "performed_by") private Long performedBy; @@ -54,4 +60,16 @@ public class CashExchange { @Column(name = "created_at") private LocalDateTime createdAt; + + @Column(name = "is_reversed") + private Boolean isReversed; + + @Column(name = "reversed_by") + private Long reversedBy; + + @Column(name = "reversed_at") + private LocalDateTime reversedAt; + + @Column(name = "reverse_reason", length = 500) + private String reverseReason; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreconciliation/api/CashReconciliationApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreconciliation/api/CashReconciliationApiResource.java index 05e9e6207..9d8cf0325 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreconciliation/api/CashReconciliationApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreconciliation/api/CashReconciliationApiResource.java @@ -122,15 +122,37 @@ public class CashReconciliationApiResource { + " JOIN m_cashiers mc ON mc.staff_id = au.staff_id WHERE mc.id = ?) " + "GROUP BY std.denomination_id, std.denomination_value, dm.kind"; + // In: cash exchange — customer gave denomination TO cashier (RECEIVED direction) + final String sqlIn4 = "SELECT ced.denomination_id, ced.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(ced.count) AS total_count " + + "FROM m_cash_exchange_detail ced " + + "JOIN m_cash_exchange ce ON ce.id = ced.exchange_id " + + "LEFT JOIN m_denomination_master dm ON dm.id = ced.denomination_id " + + "WHERE ce.cashier_id = ? AND ced.direction = 'RECEIVED' " + + "AND (ce.is_reversed IS NULL OR ce.is_reversed = false) " + + "AND ce.exchange_date <= CAST(? AS DATE) " + + "GROUP BY ced.denomination_id, ced.denomination_value, dm.kind"; + + // Out: cash exchange — cashier gave denomination TO customer (GIVEN direction) + final String sqlOut4 = "SELECT ced.denomination_id, ced.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(ced.count) AS total_count " + + "FROM m_cash_exchange_detail ced " + + "JOIN m_cash_exchange ce ON ce.id = ced.exchange_id " + + "LEFT JOIN m_denomination_master dm ON dm.id = ced.denomination_id " + + "WHERE ce.cashier_id = ? AND ced.direction = 'GIVEN' " + + "AND (ce.is_reversed IS NULL OR ce.is_reversed = false) " + + "AND ce.exchange_date <= CAST(? AS DATE) " + + "GROUP BY ced.denomination_id, ced.denomination_value, dm.kind"; + // Balance map: denomination_id → long[3] {denomValue, kindFlag (0=note/1=coin), netCount} Map balance = new LinkedHashMap<>(); addToBalance(balance, jdbcTemplate.query(sqlIn1, DENOM_ROW, cashierId, dateFilter), 1); addToBalance(balance, jdbcTemplate.query(sqlIn2, DENOM_ROW, cashierId, dateFilter), 1); addToBalance(balance, jdbcTemplate.query(sqlIn3, DENOM_ROW, dateFilter, cashierId), 1); + addToBalance(balance, jdbcTemplate.query(sqlIn4, DENOM_ROW, cashierId, dateFilter), 1); addToBalance(balance, jdbcTemplate.query(sqlOut1, DENOM_ROW, cashierId, dateFilter), -1); addToBalance(balance, jdbcTemplate.query(sqlOut2, DENOM_ROW, cashierId, dateFilter), -1); addToBalance(balance, jdbcTemplate.query(sqlOut3, DENOM_ROW, dateFilter, cashierId), -1); + addToBalance(balance, jdbcTemplate.query(sqlOut4, DENOM_ROW, cashierId, dateFilter), -1); List> rows = new ArrayList<>(); balance.entrySet().stream() diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreport/api/CashReportApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreport/api/CashReportApiResource.java index ebb53d31e..2970c9fbf 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreport/api/CashReportApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreport/api/CashReportApiResource.java @@ -69,12 +69,45 @@ public class CashReportApiResource { withdrawalTotal = withdrawalTotal.add((BigDecimal) row.get("withdrawalAmount")); } + // Cash exchange rows — separate section (denomination swap, net monetary = 0) + final String exchangeSql = "SELECT ced.denomination_id, ced.denomination_value, COALESCE(dm.kind, 'note') AS kind, " + + "SUM(CASE WHEN ced.direction = 'RECEIVED' THEN ced.count ELSE 0 END) AS received_count, " + + "SUM(CASE WHEN ced.direction = 'RECEIVED' THEN ced.total_amount ELSE 0 END) AS received_amount, " + + "SUM(CASE WHEN ced.direction = 'GIVEN' THEN ced.count ELSE 0 END) AS given_count, " + + "SUM(CASE WHEN ced.direction = 'GIVEN' THEN ced.total_amount ELSE 0 END) AS given_amount " + + "FROM m_cash_exchange_detail ced " + + "JOIN m_cash_exchange ce ON ce.id = ced.exchange_id " + + "LEFT JOIN m_denomination_master dm ON dm.id = ced.denomination_id " + + "WHERE ce.exchange_date = CAST(? AS DATE) " + + "AND (ce.is_reversed IS NULL OR ce.is_reversed = false) " + + "GROUP BY ced.denomination_id, ced.denomination_value, dm.kind " + + "ORDER BY ced.denomination_value DESC, dm.kind"; + + final List> exchangeRows = jdbcTemplate.query(exchangeSql, (rs, rowNum) -> { + long receivedCount = rs.getLong("received_count"); + long givenCount = rs.getLong("given_count"); + BigDecimal receivedAmount = rs.getBigDecimal("received_amount"); + BigDecimal givenAmount = rs.getBigDecimal("given_amount"); + Map row = new LinkedHashMap<>(); + row.put("denominationId", rs.getLong("denomination_id")); + row.put("denominationValue", rs.getInt("denomination_value")); + row.put("kind", rs.getString("kind")); + row.put("receivedCount", receivedCount); + row.put("receivedAmount", receivedAmount); + row.put("givenCount", givenCount); + row.put("givenAmount", givenAmount); + row.put("netCount", receivedCount - givenCount); + row.put("netAmount", receivedAmount.subtract(givenAmount)); + return row; + }, txnDate); + Map result = new LinkedHashMap<>(); result.put("date", txnDate); result.put("rows", rows); result.put("depositTotal", depositTotal); result.put("withdrawalTotal", withdrawalTotal); result.put("netTotal", depositTotal.subtract(withdrawalTotal)); + result.put("exchangeRows", exchangeRows); return gson.toJson(result); } @@ -83,13 +116,13 @@ public class CashReportApiResource { public String excess(@QueryParam("threshold") BigDecimal threshold, @QueryParam("fromDate") String fromDate, @QueryParam("toDate") String toDate) { final BigDecimal effectiveThreshold = threshold != null ? threshold : BigDecimal.valueOf(50000); + final boolean hasDateRange = fromDate != null && !fromDate.isBlank() && toDate != null && !toDate.isBlank(); + // Savings / Current / FD / RD transactions final StringBuilder sql = new StringBuilder(); sql.append("SELECT txn_id, MAX(account_no) account_no, MAX(client_id) client_id, MAX(cif_no) cif_no, "); sql.append("MAX(txn_date) txn_date, SUM(total_amount) total_amount FROM m_savings_txn_denomination "); - final List args = new ArrayList<>(); - final boolean hasDateRange = fromDate != null && !fromDate.isBlank() && toDate != null && !toDate.isBlank(); if (hasDateRange) { sql.append("WHERE txn_date BETWEEN CAST(? AS DATE) AND CAST(? AS DATE) "); args.add(fromDate); @@ -98,19 +131,55 @@ public class CashReportApiResource { sql.append("GROUP BY txn_id HAVING SUM(total_amount) >= ? ORDER BY total_amount DESC"); args.add(effectiveThreshold); - final List> result = jdbcTemplate.query(sql.toString(), (rs, rowNum) -> { + final List> savingsRows = jdbcTemplate.query(sql.toString(), (rs, rowNum) -> { Map row = new LinkedHashMap<>(); + row.put("type", "SAVINGS_TXN"); row.put("txnId", rs.getLong("txn_id")); row.put("accountNo", rs.getString("account_no")); row.put("clientId", rs.getObject("client_id")); row.put("cifNo", rs.getString("cif_no")); - final java.sql.Date txnDate = rs.getDate("txn_date"); - row.put("txnDate", txnDate != null ? txnDate.toString() : null); + final java.sql.Date d = rs.getDate("txn_date"); + row.put("txnDate", d != null ? d.toString() : null); row.put("totalAmount", rs.getBigDecimal("total_amount")); + row.put("idProof", null); return row; }, args.toArray()); - return gson.toJson(result); + // Cash exchange transactions + final StringBuilder exSql = new StringBuilder(); + exSql.append("SELECT id, account_no, client_id, exchange_date, exchange_amount, customer_id_proof, performed_by_username "); + exSql.append("FROM m_cash_exchange "); + exSql.append("WHERE (is_reversed IS NULL OR is_reversed = false) AND exchange_amount >= ? "); + final List exArgs = new ArrayList<>(); + exArgs.add(effectiveThreshold); + if (hasDateRange) { + exSql.append("AND exchange_date BETWEEN CAST(? AS DATE) AND CAST(? AS DATE) "); + exArgs.add(fromDate); + exArgs.add(toDate); + } + exSql.append("ORDER BY exchange_amount DESC"); + + final List> exchangeRows = jdbcTemplate.query(exSql.toString(), (rs, rowNum) -> { + Map row = new LinkedHashMap<>(); + row.put("type", "EXCHANGE"); + row.put("txnId", rs.getLong("id")); + row.put("accountNo", rs.getString("account_no")); + row.put("clientId", rs.getObject("client_id")); + row.put("cifNo", null); + final java.sql.Date d = rs.getDate("exchange_date"); + row.put("txnDate", d != null ? d.toString() : null); + row.put("totalAmount", rs.getBigDecimal("exchange_amount")); + row.put("idProof", rs.getString("customer_id_proof")); + return row; + }, exArgs.toArray()); + + // Merge and sort by amount DESC + final List> combined = new ArrayList<>(); + combined.addAll(savingsRows); + combined.addAll(exchangeRows); + combined.sort((a, b) -> ((BigDecimal) b.get("totalAmount")).compareTo((BigDecimal) a.get("totalAmount"))); + + return gson.toJson(combined); } /** @@ -132,6 +201,7 @@ public class CashReportApiResource { + " UNION SELECT from_cashier_id FROM m_cash_settlement WHERE from_cashier_id IS NOT NULL " + " UNION SELECT from_cashier_id FROM m_cash_inter_cashier WHERE from_cashier_id IS NOT NULL " + " UNION SELECT to_cashier_id FROM m_cash_inter_cashier WHERE to_cashier_id IS NOT NULL " + + " UNION SELECT cashier_id FROM m_cash_exchange WHERE cashier_id IS NOT NULL " + ") ids"; List cashierIds = jdbcTemplate.queryForList(idSql, Long.class); @@ -279,15 +349,37 @@ public class CashReportApiResource { + " JOIN m_cashiers mc ON mc.staff_id = au.staff_id WHERE mc.id = ?) " + "GROUP BY std.denomination_id, std.denomination_value, dm.kind"; + // Exchange IN: customer gave denomination to cashier (RECEIVED direction) + final String sqlIn4 = "SELECT ced.denomination_id, ced.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(ced.count) AS total_count " + + "FROM m_cash_exchange_detail ced " + + "JOIN m_cash_exchange ce ON ce.id = ced.exchange_id " + + "LEFT JOIN m_denomination_master dm ON dm.id = ced.denomination_id " + + "WHERE ce.cashier_id = ? AND ced.direction = 'RECEIVED' " + + "AND (ce.is_reversed IS NULL OR ce.is_reversed = false) " + + "AND ce.exchange_date <= CAST(? AS DATE) " + + "GROUP BY ced.denomination_id, ced.denomination_value, dm.kind"; + + // Exchange OUT: cashier gave denomination to customer (GIVEN direction) + final String sqlOut4 = "SELECT ced.denomination_id, ced.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(ced.count) AS total_count " + + "FROM m_cash_exchange_detail ced " + + "JOIN m_cash_exchange ce ON ce.id = ced.exchange_id " + + "LEFT JOIN m_denomination_master dm ON dm.id = ced.denomination_id " + + "WHERE ce.cashier_id = ? AND ced.direction = 'GIVEN' " + + "AND (ce.is_reversed IS NULL OR ce.is_reversed = false) " + + "AND ce.exchange_date <= CAST(? AS DATE) " + + "GROUP BY ced.denomination_id, ced.denomination_value, dm.kind"; + // Balance map: denomination_id → long[3] {denomValue, kindFlag (0=note/1=coin), netCount} Map balance = new LinkedHashMap<>(); addToBalance(balance, jdbcTemplate.query(sqlIn1, DENOM_ROW, cashierId, dateFilter), 1); addToBalance(balance, jdbcTemplate.query(sqlIn2, DENOM_ROW, cashierId, dateFilter), 1); addToBalance(balance, jdbcTemplate.query(sqlIn3, DENOM_ROW, dateFilter, cashierId), 1); + addToBalance(balance, jdbcTemplate.query(sqlIn4, DENOM_ROW, cashierId, dateFilter), 1); addToBalance(balance, jdbcTemplate.query(sqlOut1, DENOM_ROW, cashierId, dateFilter), -1); addToBalance(balance, jdbcTemplate.query(sqlOut2, DENOM_ROW, cashierId, dateFilter), -1); addToBalance(balance, jdbcTemplate.query(sqlOut3, DENOM_ROW, dateFilter, cashierId), -1); + addToBalance(balance, jdbcTemplate.query(sqlOut4, DENOM_ROW, cashierId, dateFilter), -1); List> rows = new ArrayList<>(); balance.entrySet().stream() diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/service/DenominationBreakdownWriter.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/service/DenominationBreakdownWriter.java index 23c4fb3db..d88f4100f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/service/DenominationBreakdownWriter.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/service/DenominationBreakdownWriter.java @@ -11,7 +11,9 @@ import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.ApiParameterError; @@ -22,6 +24,7 @@ import org.apache.fineract.infrastructure.security.service.PlatformSecurityConte import org.apache.fineract.portfolio.denominationmaster.domain.SavingsTxnDenomination; import org.apache.fineract.portfolio.denominationmaster.domain.SavingsTxnDenominationRepository; import org.apache.fineract.useradministration.domain.AppUser; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; /** @@ -34,6 +37,7 @@ public class DenominationBreakdownWriter { private final SavingsTxnDenominationRepository repository; private final PlatformSecurityContext context; + private final JdbcTemplate jdbcTemplate; // ── Public API ──────────────────────────────────────────────────────────── @@ -139,6 +143,119 @@ public class DenominationBreakdownWriter { validateBreakdown(command, expectedAmount, "TRANSACTION"); } + /** + * Validates that the cashier currently holds enough of each denomination + * in the withdrawal breakdown. Auto-detects the cashier from the authenticated + * user. If the user is not linked to a cashier (e.g. admin/back-office), the + * check is skipped silently. + * + * @param command the JSON command containing the denominationBreakdown array + */ + public void validateWithdrawalStock(final JsonCommand command) { + if (command == null || !command.parameterExists("denominationBreakdown")) return; + final JsonArray breakdown = command.arrayOfParameterNamed("denominationBreakdown"); + if (breakdown == null || breakdown.size() == 0) return; + + Long cashierId = null; + try { + final AppUser user = context.authenticatedUser(); + cashierId = jdbcTemplate.queryForObject( + "SELECT mc.id FROM m_cashiers mc JOIN m_staff ms ON ms.id = mc.staff_id " + + "JOIN m_appuser au ON au.staff_id = ms.id WHERE au.id = ?", + Long.class, user.getId()); + } catch (Exception e) { + return; // not a cashier user — skip stock check + } + if (cashierId == null) return; + + final Map stock = getCashierNetPosition(cashierId); + final List shortages = new ArrayList<>(); + + for (final JsonElement el : breakdown) { + final JsonObject item = el.getAsJsonObject(); + final int count = item.has("count") && !item.get("count").isJsonNull() ? item.get("count").getAsInt() : 0; + if (count <= 0) continue; + if (!item.has("denominationId") || item.get("denominationId").isJsonNull()) continue; + + final long denomId = item.get("denominationId").getAsLong(); + final long available = stock.getOrDefault(denomId, 0L); + if (count > available) { + final int denomValue = item.has("denominationValue") && !item.get("denominationValue").isJsonNull() + ? item.get("denominationValue").getAsInt() : 0; + shortages.add("₹" + denomValue + " (need " + count + ", available " + available + ")"); + } + } + + if (!shortages.isEmpty()) { + throw new PlatformDataIntegrityException( + "error.msg.cashier.denomination.insufficient", + "Insufficient denomination stock for withdrawal: " + String.join("; ", shortages)); + } + } + + /** + * Computes the cashier's current net denomination-wise stock by summing all IN + * transactions and subtracting all OUT transactions for that cashier. + * + * @param cashierId the cashier whose position to compute + * @return map of denominationId → net count available + */ + public Map getCashierNetPosition(final Long cashierId) { + final String sql = "SELECT denomination_id, SUM(net_count) AS available FROM (" + + " SELECT ctd.denomination_id, CAST(SUM(ctd.count) AS BIGINT) AS net_count" + + " FROM m_cash_transfer_detail ctd JOIN m_cash_transfer ct ON ct.id = ctd.transfer_id" + + " WHERE ct.to_cashier_id = ? AND (ct.is_reversed IS NULL OR ct.is_reversed = false)" + + " GROUP BY ctd.denomination_id" + + " UNION ALL" + + " SELECT icd.denomination_id, CAST(SUM(icd.count) AS BIGINT)" + + " FROM m_cash_inter_cashier_detail icd JOIN m_cash_inter_cashier ic ON ic.id = icd.inter_cashier_id" + + " WHERE ic.to_cashier_id = ? AND (ic.is_reversed IS NULL OR ic.is_reversed = false)" + + " GROUP BY icd.denomination_id" + + " UNION ALL" + + " SELECT std.denomination_id, CAST(SUM(std.count) AS BIGINT)" + + " FROM m_savings_txn_denomination std" + + " WHERE std.txn_type = 'DEPOSIT'" + + " AND std.officer_id IN (SELECT au.id FROM m_appuser au" + + " JOIN m_cashiers mc ON mc.staff_id = au.staff_id WHERE mc.id = ?)" + + " GROUP BY std.denomination_id" + + " UNION ALL" + + " SELECT ced.denomination_id, CAST(SUM(ced.count) AS BIGINT)" + + " FROM m_cash_exchange_detail ced JOIN m_cash_exchange ce ON ce.id = ced.exchange_id" + + " WHERE ce.cashier_id = ? AND ced.direction = 'RECEIVED'" + + " AND (ce.is_reversed IS NULL OR ce.is_reversed = false)" + + " GROUP BY ced.denomination_id" + + " UNION ALL" + + " SELECT csd.denomination_id, CAST(-SUM(csd.count) AS BIGINT)" + + " FROM m_cash_settlement_detail csd JOIN m_cash_settlement cs ON cs.id = csd.settlement_id" + + " WHERE cs.from_cashier_id = ? AND (cs.is_reversed IS NULL OR cs.is_reversed = false)" + + " GROUP BY csd.denomination_id" + + " UNION ALL" + + " SELECT icd.denomination_id, CAST(-SUM(icd.count) AS BIGINT)" + + " FROM m_cash_inter_cashier_detail icd JOIN m_cash_inter_cashier ic ON ic.id = icd.inter_cashier_id" + + " WHERE ic.from_cashier_id = ? AND (ic.is_reversed IS NULL OR ic.is_reversed = false)" + + " GROUP BY icd.denomination_id" + + " UNION ALL" + + " SELECT std.denomination_id, CAST(-SUM(std.count) AS BIGINT)" + + " FROM m_savings_txn_denomination std" + + " WHERE std.txn_type = 'WITHDRAWAL'" + + " AND std.officer_id IN (SELECT au.id FROM m_appuser au" + + " JOIN m_cashiers mc ON mc.staff_id = au.staff_id WHERE mc.id = ?)" + + " GROUP BY std.denomination_id" + + " UNION ALL" + + " SELECT ced.denomination_id, CAST(-SUM(ced.count) AS BIGINT)" + + " FROM m_cash_exchange_detail ced JOIN m_cash_exchange ce ON ce.id = ced.exchange_id" + + " WHERE ce.cashier_id = ? AND ced.direction = 'GIVEN'" + + " AND (ce.is_reversed IS NULL OR ce.is_reversed = false)" + + " GROUP BY ced.denomination_id" + + ") t GROUP BY denomination_id"; + + final Map stock = new HashMap<>(); + jdbcTemplate.query(sql, rs -> { + stock.put(rs.getLong("denomination_id"), rs.getLong("available")); + }, cashierId, cashierId, cashierId, cashierId, cashierId, cashierId, cashierId, cashierId); + return stock; + } + /** * Persist each valid denomination row for the given transaction. *

diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/CurrentAccountTransactionWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/CurrentAccountTransactionWritePlatformServiceImpl.java index 09b0ec6d5..adff716e1 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/CurrentAccountTransactionWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/CurrentAccountTransactionWritePlatformServiceImpl.java @@ -143,7 +143,8 @@ public class CurrentAccountTransactionWritePlatformServiceImpl implements Curren account.setTotalWithdrawalsDerived(safeBalance(account.getTotalWithdrawalsDerived()).add(amount)); currentAccountRepository.save(account); - // Save denomination breakdown for WITHDRAWAL if provided + // Validate denomination stock availability then validate totals before saving breakdown + this.denominationBreakdownWriter.validateWithdrawalStock(command); this.denominationBreakdownWriter.validateBreakdown(command, amount, "WITHDRAWAL"); this.denominationBreakdownWriter.save(command, txn.getId(), account.getId(), account.getAccountNo(), account.getClient() != null ? account.getClient().getId() : null, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java index c5190d7c6..db0944fc4 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java @@ -472,7 +472,8 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo final SavingsAccountTransaction withdrawal = this.depositAccountDomainService.handleWithdrawal(account, fmt, transactionDate, transactionAmount, paymentDetail, true, isRegularTransaction); - // Validate and save denomination breakdown for WITHDRAWAL if provided + // Validate denomination stock availability then validate totals before saving breakdown + this.denominationBreakdownWriter.validateWithdrawalStock(command); this.denominationBreakdownWriter.validateBreakdown(command, transactionAmount, "WITHDRAWAL"); this.denominationBreakdownWriter.save(command, withdrawal.getId(), account.getId(), account.getAccountNumber(), account.clientId(), account.getClient() != null ? account.getClient().getCifNumber() : null, transactionDate, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java index 0ee80cdeb..380ac199b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java @@ -88,8 +88,8 @@ import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlat import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService; import org.apache.fineract.portfolio.charge.domain.Charge; import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper; -import org.apache.fineract.portfolio.denominationmaster.service.DenominationBreakdownWriter; import org.apache.fineract.portfolio.charge.domain.ChargeTimeType; +import org.apache.fineract.portfolio.denominationmaster.service.DenominationBreakdownWriter; import org.apache.fineract.portfolio.client.domain.Client; import org.apache.fineract.portfolio.client.exception.ClientNotActiveException; import org.apache.fineract.portfolio.group.domain.Group; @@ -424,7 +424,8 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi gsimRepository.save(gsim); } - // Validate and save denomination breakdown for WITHDRAWAL via shared writer + // Validate denomination stock availability then validate totals before saving breakdown + this.denominationBreakdownWriter.validateWithdrawalStock(command); this.denominationBreakdownWriter.validateBreakdown(command, transactionAmount, "WITHDRAWAL"); this.denominationBreakdownWriter.save(command, withdrawal.getId(), account.getId(), account.getAccountNumber(), account.clientId(), account.getClient() != null ? account.getClient().getCifNumber() : null,