From b25d5a30d26eaaa33650e2eb1c916af9d4b3375c Mon Sep 17 00:00:00 2001 From: Jaanu423 Date: Wed, 20 May 2026 18:26:15 +0530 Subject: [PATCH 1/3] feat: FD/RD GL transaction prefixes + savingsAccountNo in journal entry API - Use FD/RD transaction ID prefixes for Fixed/Recurring Deposit GL entries via ThreadLocal context set in Cash/AccrualBasedAccountingProcessorForSavings - Add depositAccountType to SavingsDTO and pass from SavingsAccount bridge data - Add savingsAccountNo field to JournalEntryData; populate via LEFT JOIN on m_savings_account (SB/FD/RD) and m_current_account (CA prefix) in read query - Fix JournalEntryMapper MapStruct: ignore savingsAccountNo (SQL-populated) - Fix JournalEntryRunningBalanceUpdateServiceImpl constructor call (27 params) Co-Authored-By: Claude Sonnet 4.6 --- .../journalentry/JournalEntryMapper.java | 217 +- .../journalentry/data/JournalEntryData.java | 578 +- .../journalentry/data/SavingsDTO.java | 77 +- .../service/AccountingProcessorHelper.java | 2795 +++--- ...ualBasedAccountingProcessorForSavings.java | 575 +- ...ashBasedAccountingProcessorForSavings.java | 515 +- .../JournalEntryReadPlatformServiceImpl.java | 1522 ++-- ...lEntryRunningBalanceUpdateServiceImpl.java | 570 +- .../savings/domain/SavingsAccount.java | 8087 +++++++++-------- 9 files changed, 7492 insertions(+), 7444 deletions(-) diff --git a/fineract-accounting/src/main/java/org/apache/fineract/accounting/journalentry/JournalEntryMapper.java b/fineract-accounting/src/main/java/org/apache/fineract/accounting/journalentry/JournalEntryMapper.java index 1ff714948..7abb384c7 100644 --- a/fineract-accounting/src/main/java/org/apache/fineract/accounting/journalentry/JournalEntryMapper.java +++ b/fineract-accounting/src/main/java/org/apache/fineract/accounting/journalentry/JournalEntryMapper.java @@ -1,108 +1,109 @@ -/** - * 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.accounting.journalentry; - -import java.time.OffsetDateTime; -import org.apache.fineract.accounting.glaccount.domain.GLAccountType; -import org.apache.fineract.accounting.journalentry.data.JournalEntryData; -import org.apache.fineract.accounting.journalentry.domain.JournalEntry; -import org.apache.fineract.accounting.journalentry.domain.JournalEntryType; -import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig; -import org.apache.fineract.infrastructure.core.data.EnumOptionData; -import org.apache.fineract.organisation.monetary.data.CurrencyData; -import org.apache.fineract.portfolio.PortfolioProductType; -import org.mapstruct.Mapper; -import org.mapstruct.Mapping; -import org.mapstruct.Named; - -@Mapper(config = MapstructMapperConfig.class) -public interface JournalEntryMapper { - - @Mapping(target = "id", source = "id") - @Mapping(target = "officeId", source = "office.id") - @Mapping(target = "officeName", source = "office.name") - @Mapping(target = "glAccountId", source = "glAccount.id") - @Mapping(target = "glAccountCode", source = "glAccount.glCode") - @Mapping(target = "glAccountName", source = "glAccount.name") - @Mapping(target = "glAccountType", source = "glAccount.type", qualifiedByName = "glAccountType") - @Mapping(target = "transactionDate", source = "transactionDate") - @Mapping(target = "entryType", source = "type", qualifiedByName = "journalEntryType") - @Mapping(target = "amount", source = "amount") - @Mapping(target = "entityType", source = "entityType", qualifiedByName = "entityType") - @Mapping(target = "entityId", source = "entityId") - @Mapping(target = "submittedOnDate", expression = "java(journalEntry.getCreatedDate().orElse(null))") - @Mapping(target = "transactionId", source = "transactionId") - @Mapping(target = "currency", source = "currencyCode") - @Mapping(target = "manualEntry", source = "manualEntry") - @Mapping(target = "reversed", source = "reversed") - @Mapping(target = "referenceNumber", source = "referenceNumber") - @Mapping(target = "paymentTypeId", source = "paymentDetail.paymentType.id") - @Mapping(target = "accountNumber", source = "paymentDetail.accountNumber") - @Mapping(target = "checkNumber", source = "paymentDetail.checkNumber") - @Mapping(target = "routingCode", source = "paymentDetail.routingCode") - @Mapping(target = "receiptNumber", source = "paymentDetail.receiptNumber") - @Mapping(target = "bankNumber", source = "paymentDetail.bankNumber") - @Mapping(target = "createdDate", ignore = true) - @Mapping(target = "createdByUserId", ignore = true) - @Mapping(target = "createdByUserName", ignore = true) - @Mapping(target = "comments", ignore = true) - @Mapping(target = "officeRunningBalance", ignore = true) - @Mapping(target = "organizationRunningBalance", ignore = true) - @Mapping(target = "runningBalanceComputed", ignore = true) - @Mapping(target = "rowIndex", ignore = true) - @Mapping(target = "dateFormat", ignore = true) - @Mapping(target = "locale", ignore = true) - @Mapping(target = "credits", ignore = true) - @Mapping(target = "debits", ignore = true) - @Mapping(target = "transactionDetails", ignore = true) - @Mapping(target = "savingTransactionId", ignore = true) - @Mapping(target = "externalAssetOwner", ignore = true) - JournalEntryData map(JournalEntry journalEntry); - - @Named("entityType") - default PortfolioProductType mapEntityType(Integer entityTypeId) { - return PortfolioProductType.fromInt(entityTypeId); - } - - @Named("glAccountType") - default GLAccountType mapGlAccountType(Integer typeId) { - return GLAccountType.fromInt(typeId); - } - - @Named("journalEntryType") - default JournalEntryType mapJournalEntryType(Integer typeId) { - return JournalEntryType.fromInt(typeId); - } - - default EnumOptionData mapGlAccountType(GLAccountType accountType) { - return new EnumOptionData((long) accountType.getValue(), accountType.getCode(), accountType.name()); - } - - default EnumOptionData mapJournalEntryType(JournalEntryType journalEntryType) { - return new EnumOptionData((long) journalEntryType.getValue(), journalEntryType.getCode(), journalEntryType.name()); - } - - default EnumOptionData mapEntityType(PortfolioProductType entityType) { - return new EnumOptionData((long) entityType.getValue(), entityType.getCode(), entityType.name()); - } - - default CurrencyData mapCurrency(String currencyCode) { - return new CurrencyData(currencyCode); - } -} +/** + * 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.accounting.journalentry; + +import java.time.OffsetDateTime; +import org.apache.fineract.accounting.glaccount.domain.GLAccountType; +import org.apache.fineract.accounting.journalentry.data.JournalEntryData; +import org.apache.fineract.accounting.journalentry.domain.JournalEntry; +import org.apache.fineract.accounting.journalentry.domain.JournalEntryType; +import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig; +import org.apache.fineract.infrastructure.core.data.EnumOptionData; +import org.apache.fineract.organisation.monetary.data.CurrencyData; +import org.apache.fineract.portfolio.PortfolioProductType; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; + +@Mapper(config = MapstructMapperConfig.class) +public interface JournalEntryMapper { + + @Mapping(target = "id", source = "id") + @Mapping(target = "officeId", source = "office.id") + @Mapping(target = "officeName", source = "office.name") + @Mapping(target = "glAccountId", source = "glAccount.id") + @Mapping(target = "glAccountCode", source = "glAccount.glCode") + @Mapping(target = "glAccountName", source = "glAccount.name") + @Mapping(target = "glAccountType", source = "glAccount.type", qualifiedByName = "glAccountType") + @Mapping(target = "transactionDate", source = "transactionDate") + @Mapping(target = "entryType", source = "type", qualifiedByName = "journalEntryType") + @Mapping(target = "amount", source = "amount") + @Mapping(target = "entityType", source = "entityType", qualifiedByName = "entityType") + @Mapping(target = "entityId", source = "entityId") + @Mapping(target = "submittedOnDate", expression = "java(journalEntry.getCreatedDate().orElse(null))") + @Mapping(target = "transactionId", source = "transactionId") + @Mapping(target = "currency", source = "currencyCode") + @Mapping(target = "manualEntry", source = "manualEntry") + @Mapping(target = "reversed", source = "reversed") + @Mapping(target = "referenceNumber", source = "referenceNumber") + @Mapping(target = "paymentTypeId", source = "paymentDetail.paymentType.id") + @Mapping(target = "accountNumber", source = "paymentDetail.accountNumber") + @Mapping(target = "checkNumber", source = "paymentDetail.checkNumber") + @Mapping(target = "routingCode", source = "paymentDetail.routingCode") + @Mapping(target = "receiptNumber", source = "paymentDetail.receiptNumber") + @Mapping(target = "bankNumber", source = "paymentDetail.bankNumber") + @Mapping(target = "createdDate", ignore = true) + @Mapping(target = "createdByUserId", ignore = true) + @Mapping(target = "createdByUserName", ignore = true) + @Mapping(target = "comments", ignore = true) + @Mapping(target = "officeRunningBalance", ignore = true) + @Mapping(target = "organizationRunningBalance", ignore = true) + @Mapping(target = "runningBalanceComputed", ignore = true) + @Mapping(target = "rowIndex", ignore = true) + @Mapping(target = "dateFormat", ignore = true) + @Mapping(target = "locale", ignore = true) + @Mapping(target = "credits", ignore = true) + @Mapping(target = "debits", ignore = true) + @Mapping(target = "transactionDetails", ignore = true) + @Mapping(target = "savingTransactionId", ignore = true) + @Mapping(target = "externalAssetOwner", ignore = true) + @Mapping(target = "savingsAccountNo", ignore = true) + JournalEntryData map(JournalEntry journalEntry); + + @Named("entityType") + default PortfolioProductType mapEntityType(Integer entityTypeId) { + return PortfolioProductType.fromInt(entityTypeId); + } + + @Named("glAccountType") + default GLAccountType mapGlAccountType(Integer typeId) { + return GLAccountType.fromInt(typeId); + } + + @Named("journalEntryType") + default JournalEntryType mapJournalEntryType(Integer typeId) { + return JournalEntryType.fromInt(typeId); + } + + default EnumOptionData mapGlAccountType(GLAccountType accountType) { + return new EnumOptionData((long) accountType.getValue(), accountType.getCode(), accountType.name()); + } + + default EnumOptionData mapJournalEntryType(JournalEntryType journalEntryType) { + return new EnumOptionData((long) journalEntryType.getValue(), journalEntryType.getCode(), journalEntryType.name()); + } + + default EnumOptionData mapEntityType(PortfolioProductType entityType) { + return new EnumOptionData((long) entityType.getValue(), entityType.getCode(), entityType.name()); + } + + default CurrencyData mapCurrency(String currencyCode) { + return new CurrencyData(currencyCode); + } +} diff --git a/fineract-accounting/src/main/java/org/apache/fineract/accounting/journalentry/data/JournalEntryData.java b/fineract-accounting/src/main/java/org/apache/fineract/accounting/journalentry/data/JournalEntryData.java index 9f1e5b8a8..06944b0e4 100644 --- a/fineract-accounting/src/main/java/org/apache/fineract/accounting/journalentry/data/JournalEntryData.java +++ b/fineract-accounting/src/main/java/org/apache/fineract/accounting/journalentry/data/JournalEntryData.java @@ -1,287 +1,291 @@ -/** - * 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.accounting.journalentry.data; - -import java.math.BigDecimal; -import java.time.LocalDate; -import java.time.OffsetDateTime; -import java.util.List; -import lombok.Data; -import org.apache.fineract.accounting.glaccount.data.GLAccountData; -import org.apache.fineract.infrastructure.core.data.EnumOptionData; -import org.apache.fineract.organisation.monetary.data.CurrencyData; - -/** - * Immutable object representing a General Ledger Account - * - * Note: no getter/setters required as google will produce json from fields of object. - */ -@Data -public class JournalEntryData { - - private Long id; - private Long officeId; - @SuppressWarnings("unused") - private String officeName; - @SuppressWarnings("unused") - private String glAccountName; - private Long glAccountId; - @SuppressWarnings("unused") - private String glAccountCode; - private EnumOptionData glAccountType; - @SuppressWarnings("unused") - private LocalDate transactionDate; - private EnumOptionData entryType; - private BigDecimal amount; - @SuppressWarnings("unused") - private CurrencyData currency; - private String transactionId; - @SuppressWarnings("unused") - private Boolean manualEntry; - @SuppressWarnings("unused") - private EnumOptionData entityType; - @SuppressWarnings("unused") - private Long entityId; - @SuppressWarnings("unused") - private Long createdByUserId; - @SuppressWarnings("unused") - private OffsetDateTime createdDate; - @SuppressWarnings("unused") - private String createdByUserName; - @SuppressWarnings("unused") - private String comments; - @SuppressWarnings("unused") - private Boolean reversed; - @SuppressWarnings("unused") - private String referenceNumber; - @SuppressWarnings("unused") - private BigDecimal officeRunningBalance; - @SuppressWarnings("unused") - private BigDecimal organizationRunningBalance; - @SuppressWarnings("unused") - private Boolean runningBalanceComputed; - - @SuppressWarnings("unused") - private TransactionDetailData transactionDetails; - @SuppressWarnings("unused") - private OffsetDateTime submittedOnDate; - - // import fields - private transient Integer rowIndex; - private String dateFormat; - private String locale; - private List credits; - private List debits; - private Long paymentTypeId; - private String currencyCode; - private String accountNumber; - private String checkNumber; - private String routingCode; - private String receiptNumber; - private String bankNumber; - private String externalAssetOwner; - private transient Long savingTransactionId; - - public JournalEntryData() {} - - // for opening bal bulk import - public JournalEntryData(Long officeId, LocalDate transactionDate, String currencyCode, List credits, - List debits, String locale, String dateFormat) { - this.officeId = officeId; - this.dateFormat = dateFormat; - this.locale = locale; - this.transactionDate = transactionDate; - this.currencyCode = currencyCode; - this.credits = credits; - this.debits = debits; - this.rowIndex = null; - this.paymentTypeId = null; - this.accountNumber = null; - this.checkNumber = null; - this.routingCode = null; - this.receiptNumber = null; - this.bankNumber = null; - this.comments = null; - this.id = null; - this.officeName = null; - this.glAccountName = null; - this.glAccountId = null; - this.glAccountCode = null; - this.glAccountType = null; - this.entryType = null; - this.amount = null; - this.currency = null; - this.transactionId = null; - this.manualEntry = null; - this.entityType = null; - this.entityId = null; - this.createdByUserId = null; - this.createdDate = null; - this.createdByUserName = null; - this.reversed = null; - this.referenceNumber = null; - this.officeRunningBalance = null; - this.organizationRunningBalance = null; - this.runningBalanceComputed = null; - this.transactionDetails = null; - this.submittedOnDate = null; - } - - private JournalEntryData(Long officeId, LocalDate transactionDate, String currencyCode, Long paymentTypeId, Integer rowIndex, - List credits, List debits, String accountNumber, String checkNumber, String routingCode, - String receiptNumber, String bankNumber, String comments, String locale, String dateFormat) { - - this.officeId = officeId; - this.dateFormat = dateFormat; - this.locale = locale; - this.transactionDate = transactionDate; - this.currencyCode = currencyCode; - this.rowIndex = rowIndex; - this.credits = credits; - this.debits = debits; - this.paymentTypeId = paymentTypeId; - this.accountNumber = accountNumber; - this.checkNumber = checkNumber; - this.routingCode = routingCode; - this.receiptNumber = receiptNumber; - this.bankNumber = bankNumber; - this.comments = comments; - this.id = null; - this.officeName = null; - this.glAccountName = null; - this.glAccountId = null; - this.glAccountCode = null; - this.glAccountType = null; - this.entryType = null; - this.amount = null; - this.currency = null; - this.transactionId = null; - this.manualEntry = null; - this.entityType = null; - this.entityId = null; - this.createdByUserId = null; - this.submittedOnDate = null; - this.createdDate = null; - this.createdByUserName = null; - this.reversed = null; - this.referenceNumber = null; - this.officeRunningBalance = null; - this.organizationRunningBalance = null; - this.runningBalanceComputed = null; - this.transactionDetails = null; - } - - public JournalEntryData(final Long id, final Long officeId, final String officeName, final String glAccountName, final Long glAccountId, - final String glAccountCode, final EnumOptionData glAccountClassification, final LocalDate transactionDate, - final EnumOptionData entryType, final BigDecimal amount, final String transactionId, final Boolean manualEntry, - final EnumOptionData entityType, final Long entityId, final Long createdByUserId, final OffsetDateTime submittedOnDate, - final String createdByUserName, final String comments, final Boolean reversed, final String referenceNumber, - final BigDecimal officeRunningBalance, final BigDecimal organizationRunningBalance, final Boolean runningBalanceComputed, - final TransactionDetailData transactionDetailData, final CurrencyData currency, final String externalAssetOwner) { - this.id = id; - this.officeId = officeId; - this.officeName = officeName; - this.glAccountName = glAccountName; - this.glAccountId = glAccountId; - this.glAccountCode = glAccountCode; - this.glAccountType = glAccountClassification; - this.transactionDate = transactionDate; - this.entryType = entryType; - this.amount = amount; - this.transactionId = transactionId; - this.manualEntry = manualEntry; - this.entityType = entityType; - this.entityId = entityId; - this.createdByUserId = createdByUserId; - this.createdDate = submittedOnDate; - this.submittedOnDate = submittedOnDate; - this.createdByUserName = createdByUserName; - this.comments = comments; - this.reversed = reversed; - this.referenceNumber = referenceNumber; - this.officeRunningBalance = officeRunningBalance; - this.organizationRunningBalance = organizationRunningBalance; - this.runningBalanceComputed = runningBalanceComputed; - this.transactionDetails = transactionDetailData; - this.currency = currency; - this.externalAssetOwner = externalAssetOwner; - } - - public static JournalEntryData importInstance(Long officeId, LocalDate transactionDate, String currencyCode, Long paymentTypeId, - Integer rowIndex, List credits, List debits, String accountNumber, String checkNumber, - String routingCode, String receiptNumber, String bankNumber, String comments, String locale, String dateFormat) { - return new JournalEntryData(officeId, transactionDate, currencyCode, paymentTypeId, rowIndex, credits, debits, accountNumber, - checkNumber, routingCode, receiptNumber, bankNumber, comments, locale, dateFormat); - } - - public static JournalEntryData importInstance1(Long officeId, LocalDate transactionDate, String currencyCode, List credits, - List debits, String locale, String dateFormat) { - return new JournalEntryData(officeId, transactionDate, currencyCode, credits, debits, locale, dateFormat); - } - - public static JournalEntryData fromGLAccountData(final GLAccountData glAccountData) { - - final Long id = null; - final Long officeId = null; - final String officeName = null; - final String glAccountName = glAccountData.getName(); - final Long glAccountId = glAccountData.getId(); - final String glAccountCode = glAccountData.getGlCode(); - final EnumOptionData glAccountClassification = glAccountData.getType(); - final LocalDate transactionDate = null; - final EnumOptionData entryType = null; - final BigDecimal amount = null; - final String transactionId = null; - final Boolean manualEntry = null; - final EnumOptionData entityType = null; - final Long entityId = null; - final Long createdByUserId = null; - final OffsetDateTime submittedOnDate = null; - final String createdByUserName = null; - final String comments = null; - final Boolean reversed = null; - final String referenceNumber = null; - final BigDecimal officeRunningBalance = null; - final BigDecimal organizationRunningBalance = null; - final Boolean runningBalanceComputed = null; - final TransactionDetailData transactionDetailData = null; - final CurrencyData currency = null; - final String externalAssetOwner = null; - return new JournalEntryData(id, officeId, officeName, glAccountName, glAccountId, glAccountCode, glAccountClassification, - transactionDate, entryType, amount, transactionId, manualEntry, entityType, entityId, createdByUserId, submittedOnDate, - createdByUserName, comments, reversed, referenceNumber, officeRunningBalance, organizationRunningBalance, - runningBalanceComputed, transactionDetailData, currency, externalAssetOwner); - } - - public Integer getRowIndex() { - return rowIndex; - } - - public LocalDate getTransactionDate() { - return transactionDate; - } - - public void addDebits(CreditDebit debit) { - this.debits.add(debit); - } - - public void addCredits(CreditDebit credit) { - this.credits.add(credit); - } -} +/** + * 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.accounting.journalentry.data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.OffsetDateTime; +import java.util.List; +import lombok.Data; +import org.apache.fineract.accounting.glaccount.data.GLAccountData; +import org.apache.fineract.infrastructure.core.data.EnumOptionData; +import org.apache.fineract.organisation.monetary.data.CurrencyData; + +/** + * Immutable object representing a General Ledger Account + * + * Note: no getter/setters required as google will produce json from fields of object. + */ +@Data +public class JournalEntryData { + + private Long id; + private Long officeId; + @SuppressWarnings("unused") + private String officeName; + @SuppressWarnings("unused") + private String glAccountName; + private Long glAccountId; + @SuppressWarnings("unused") + private String glAccountCode; + private EnumOptionData glAccountType; + @SuppressWarnings("unused") + private LocalDate transactionDate; + private EnumOptionData entryType; + private BigDecimal amount; + @SuppressWarnings("unused") + private CurrencyData currency; + private String transactionId; + @SuppressWarnings("unused") + private Boolean manualEntry; + @SuppressWarnings("unused") + private EnumOptionData entityType; + @SuppressWarnings("unused") + private Long entityId; + @SuppressWarnings("unused") + private String savingsAccountNo; + @SuppressWarnings("unused") + private Long createdByUserId; + @SuppressWarnings("unused") + private OffsetDateTime createdDate; + @SuppressWarnings("unused") + private String createdByUserName; + @SuppressWarnings("unused") + private String comments; + @SuppressWarnings("unused") + private Boolean reversed; + @SuppressWarnings("unused") + private String referenceNumber; + @SuppressWarnings("unused") + private BigDecimal officeRunningBalance; + @SuppressWarnings("unused") + private BigDecimal organizationRunningBalance; + @SuppressWarnings("unused") + private Boolean runningBalanceComputed; + + @SuppressWarnings("unused") + private TransactionDetailData transactionDetails; + @SuppressWarnings("unused") + private OffsetDateTime submittedOnDate; + + // import fields + private transient Integer rowIndex; + private String dateFormat; + private String locale; + private List credits; + private List debits; + private Long paymentTypeId; + private String currencyCode; + private String accountNumber; + private String checkNumber; + private String routingCode; + private String receiptNumber; + private String bankNumber; + private String externalAssetOwner; + private transient Long savingTransactionId; + + public JournalEntryData() {} + + // for opening bal bulk import + public JournalEntryData(Long officeId, LocalDate transactionDate, String currencyCode, List credits, + List debits, String locale, String dateFormat) { + this.officeId = officeId; + this.dateFormat = dateFormat; + this.locale = locale; + this.transactionDate = transactionDate; + this.currencyCode = currencyCode; + this.credits = credits; + this.debits = debits; + this.rowIndex = null; + this.paymentTypeId = null; + this.accountNumber = null; + this.checkNumber = null; + this.routingCode = null; + this.receiptNumber = null; + this.bankNumber = null; + this.comments = null; + this.id = null; + this.officeName = null; + this.glAccountName = null; + this.glAccountId = null; + this.glAccountCode = null; + this.glAccountType = null; + this.entryType = null; + this.amount = null; + this.currency = null; + this.transactionId = null; + this.manualEntry = null; + this.entityType = null; + this.entityId = null; + this.createdByUserId = null; + this.createdDate = null; + this.createdByUserName = null; + this.reversed = null; + this.referenceNumber = null; + this.officeRunningBalance = null; + this.organizationRunningBalance = null; + this.runningBalanceComputed = null; + this.transactionDetails = null; + this.submittedOnDate = null; + } + + private JournalEntryData(Long officeId, LocalDate transactionDate, String currencyCode, Long paymentTypeId, Integer rowIndex, + List credits, List debits, String accountNumber, String checkNumber, String routingCode, + String receiptNumber, String bankNumber, String comments, String locale, String dateFormat) { + + this.officeId = officeId; + this.dateFormat = dateFormat; + this.locale = locale; + this.transactionDate = transactionDate; + this.currencyCode = currencyCode; + this.rowIndex = rowIndex; + this.credits = credits; + this.debits = debits; + this.paymentTypeId = paymentTypeId; + this.accountNumber = accountNumber; + this.checkNumber = checkNumber; + this.routingCode = routingCode; + this.receiptNumber = receiptNumber; + this.bankNumber = bankNumber; + this.comments = comments; + this.id = null; + this.officeName = null; + this.glAccountName = null; + this.glAccountId = null; + this.glAccountCode = null; + this.glAccountType = null; + this.entryType = null; + this.amount = null; + this.currency = null; + this.transactionId = null; + this.manualEntry = null; + this.entityType = null; + this.entityId = null; + this.createdByUserId = null; + this.submittedOnDate = null; + this.createdDate = null; + this.createdByUserName = null; + this.reversed = null; + this.referenceNumber = null; + this.officeRunningBalance = null; + this.organizationRunningBalance = null; + this.runningBalanceComputed = null; + this.transactionDetails = null; + } + + public JournalEntryData(final Long id, final Long officeId, final String officeName, final String glAccountName, final Long glAccountId, + final String glAccountCode, final EnumOptionData glAccountClassification, final LocalDate transactionDate, + final EnumOptionData entryType, final BigDecimal amount, final String transactionId, final Boolean manualEntry, + final EnumOptionData entityType, final Long entityId, final Long createdByUserId, final OffsetDateTime submittedOnDate, + final String createdByUserName, final String comments, final Boolean reversed, final String referenceNumber, + final BigDecimal officeRunningBalance, final BigDecimal organizationRunningBalance, final Boolean runningBalanceComputed, + final TransactionDetailData transactionDetailData, final CurrencyData currency, final String externalAssetOwner, + final String savingsAccountNo) { + this.id = id; + this.officeId = officeId; + this.officeName = officeName; + this.glAccountName = glAccountName; + this.glAccountId = glAccountId; + this.glAccountCode = glAccountCode; + this.glAccountType = glAccountClassification; + this.transactionDate = transactionDate; + this.entryType = entryType; + this.amount = amount; + this.transactionId = transactionId; + this.manualEntry = manualEntry; + this.entityType = entityType; + this.entityId = entityId; + this.createdByUserId = createdByUserId; + this.createdDate = submittedOnDate; + this.submittedOnDate = submittedOnDate; + this.createdByUserName = createdByUserName; + this.comments = comments; + this.reversed = reversed; + this.referenceNumber = referenceNumber; + this.officeRunningBalance = officeRunningBalance; + this.organizationRunningBalance = organizationRunningBalance; + this.runningBalanceComputed = runningBalanceComputed; + this.transactionDetails = transactionDetailData; + this.currency = currency; + this.externalAssetOwner = externalAssetOwner; + this.savingsAccountNo = savingsAccountNo; + } + + public static JournalEntryData importInstance(Long officeId, LocalDate transactionDate, String currencyCode, Long paymentTypeId, + Integer rowIndex, List credits, List debits, String accountNumber, String checkNumber, + String routingCode, String receiptNumber, String bankNumber, String comments, String locale, String dateFormat) { + return new JournalEntryData(officeId, transactionDate, currencyCode, paymentTypeId, rowIndex, credits, debits, accountNumber, + checkNumber, routingCode, receiptNumber, bankNumber, comments, locale, dateFormat); + } + + public static JournalEntryData importInstance1(Long officeId, LocalDate transactionDate, String currencyCode, List credits, + List debits, String locale, String dateFormat) { + return new JournalEntryData(officeId, transactionDate, currencyCode, credits, debits, locale, dateFormat); + } + + public static JournalEntryData fromGLAccountData(final GLAccountData glAccountData) { + + final Long id = null; + final Long officeId = null; + final String officeName = null; + final String glAccountName = glAccountData.getName(); + final Long glAccountId = glAccountData.getId(); + final String glAccountCode = glAccountData.getGlCode(); + final EnumOptionData glAccountClassification = glAccountData.getType(); + final LocalDate transactionDate = null; + final EnumOptionData entryType = null; + final BigDecimal amount = null; + final String transactionId = null; + final Boolean manualEntry = null; + final EnumOptionData entityType = null; + final Long entityId = null; + final Long createdByUserId = null; + final OffsetDateTime submittedOnDate = null; + final String createdByUserName = null; + final String comments = null; + final Boolean reversed = null; + final String referenceNumber = null; + final BigDecimal officeRunningBalance = null; + final BigDecimal organizationRunningBalance = null; + final Boolean runningBalanceComputed = null; + final TransactionDetailData transactionDetailData = null; + final CurrencyData currency = null; + final String externalAssetOwner = null; + return new JournalEntryData(id, officeId, officeName, glAccountName, glAccountId, glAccountCode, glAccountClassification, + transactionDate, entryType, amount, transactionId, manualEntry, entityType, entityId, createdByUserId, submittedOnDate, + createdByUserName, comments, reversed, referenceNumber, officeRunningBalance, organizationRunningBalance, + runningBalanceComputed, transactionDetailData, currency, externalAssetOwner, null); + } + + public Integer getRowIndex() { + return rowIndex; + } + + public LocalDate getTransactionDate() { + return transactionDate; + } + + public void addDebits(CreditDebit debit) { + this.debits.add(debit); + } + + public void addCredits(CreditDebit credit) { + this.credits.add(credit); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/SavingsDTO.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/SavingsDTO.java index 66028d83d..ed0f5edcc 100755 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/SavingsDTO.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/SavingsDTO.java @@ -1,38 +1,39 @@ -/** - * 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.accounting.journalentry.data; - -import java.util.List; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.Setter; - -@AllArgsConstructor -@Getter -@Setter -public class SavingsDTO { - - private Long savingsId; - private Long savingsProductId; - private Long officeId; - private String currencyCode; - private boolean cashBasedAccountingEnabled; - private boolean accrualBasedAccountingEnabled; - private List newSavingsTransactions; -} +/** + * 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.accounting.journalentry.data; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@AllArgsConstructor +@Getter +@Setter +public class SavingsDTO { + + private Long savingsId; + private Long savingsProductId; + private Long officeId; + private String currencyCode; + private boolean cashBasedAccountingEnabled; + private boolean accrualBasedAccountingEnabled; + private List newSavingsTransactions; + private int depositAccountType; +} 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 21e81186b..ee76982f5 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 @@ -1,1386 +1,1409 @@ -/** - * 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.accounting.journalentry.service; - -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 java.util.Objects; -import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.StringUtils; -import org.apache.fineract.accounting.closure.domain.GLClosure; -import org.apache.fineract.accounting.closure.domain.GLClosureRepository; -import org.apache.fineract.accounting.common.AccountingConstants; -import org.apache.fineract.accounting.common.AccountingConstants.AccrualAccountsForLoan; -import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForLoan; -import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForSavings; -import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForShares; -import org.apache.fineract.accounting.common.AccountingConstants.FinancialActivity; -import org.apache.fineract.accounting.common.AccountingConstants.LoanProductAccountingParams; -import org.apache.fineract.accounting.financialactivityaccount.domain.FinancialActivityAccount; -import org.apache.fineract.accounting.financialactivityaccount.domain.FinancialActivityAccountRepositoryWrapper; -import org.apache.fineract.accounting.glaccount.domain.GLAccount; -import org.apache.fineract.accounting.glaccount.domain.GLAccountRepository; -import org.apache.fineract.accounting.journalentry.data.ChargePaymentDTO; -import org.apache.fineract.accounting.journalentry.data.ClientChargePaymentDTO; -import org.apache.fineract.accounting.journalentry.data.ClientTransactionDTO; -import org.apache.fineract.accounting.journalentry.data.LoanDTO; -import org.apache.fineract.accounting.journalentry.data.LoanTransactionDTO; -import org.apache.fineract.accounting.journalentry.data.SavingsDTO; -import org.apache.fineract.accounting.journalentry.data.SavingsTransactionDTO; -import org.apache.fineract.accounting.journalentry.data.SharesDTO; -import org.apache.fineract.accounting.journalentry.data.SharesTransactionDTO; -import org.apache.fineract.accounting.journalentry.data.TaxPaymentDTO; -import org.apache.fineract.accounting.journalentry.domain.JournalEntry; -import org.apache.fineract.accounting.journalentry.domain.JournalEntryRepository; -import org.apache.fineract.accounting.journalentry.domain.JournalEntryType; -import org.apache.fineract.accounting.journalentry.exception.JournalEntryInvalidException; -import org.apache.fineract.accounting.journalentry.exception.JournalEntryInvalidException.GlJournalEntryInvalidReason; -import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMapping; -import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMappingRepository; -import org.apache.fineract.accounting.producttoaccountmapping.exception.ProductToGLAccountMappingNotFoundException; -import org.apache.fineract.infrastructure.core.data.EnumOptionData; -import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; -import org.apache.fineract.infrastructure.core.service.DateUtils; -import org.apache.fineract.infrastructure.core.service.MathUtil; -import org.apache.fineract.infrastructure.event.business.domain.journalentry.LoanJournalEntryCreatedBusinessEvent; -import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; -import org.apache.fineract.organisation.office.domain.Office; -import org.apache.fineract.organisation.office.domain.OfficeRepository; -import org.apache.fineract.portfolio.PortfolioProductType; -import org.apache.fineract.portfolio.account.PortfolioAccountType; -import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService; -import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper; -import org.apache.fineract.portfolio.loanaccount.data.AccountingBridgeLoanTransactionDTO; -import org.apache.fineract.portfolio.loanaccount.data.LoanChargeData; -import org.apache.fineract.portfolio.loanaccount.data.LoanChargePaidByDTO; -import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionEnumData; -import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionEnumData; -import org.apache.fineract.portfolio.shareaccounts.data.ShareAccountTransactionEnumData; -import org.springframework.dao.DataAccessException; - -@RequiredArgsConstructor -public class AccountingProcessorHelper { - - public static final String LOAN_TRANSACTION_IDENTIFIER = "L"; - public static final String SAVINGS_TRANSACTION_IDENTIFIER = "S"; - public static final String CLIENT_TRANSACTION_IDENTIFIER = "C"; - public static final String PROVISIONING_TRANSACTION_IDENTIFIER = "P"; - public static final String SHARE_TRANSACTION_IDENTIFIER = "SH"; - - private final JournalEntryRepository glJournalEntryRepository; - private final ProductToGLAccountMappingRepository accountMappingRepository; - private final FinancialActivityAccountRepositoryWrapper financialActivityAccountRepository; - private final GLClosureRepository closureRepository; - private final GLAccountRepository glAccountRepository; - private final OfficeRepository officeRepository; - private final AccountTransfersReadPlatformService accountTransfersReadPlatformService; - private final ChargeRepositoryWrapper chargeRepositoryWrapper; - private final BusinessEventNotifierService businessEventNotifierService; - - public LoanDTO populateLoanDtoFromDTO( - final org.apache.fineract.portfolio.loanaccount.data.AccountingBridgeDataDTO accountingBridgeData) { - final Long loanId = accountingBridgeData.getLoanId(); - final Long loanProductId = accountingBridgeData.getLoanProductId(); - final Long officeId = accountingBridgeData.getOfficeId(); - final String currencyCode = accountingBridgeData.getCurrencyCode(); - final List newLoanTransactions = new ArrayList<>(); - boolean isAccountTransfer = accountingBridgeData.isAccountTransfer(); - boolean isLoanMarkedAsChargeOff = accountingBridgeData.isChargeOff(); - boolean isLoanMarkedAsFraud = accountingBridgeData.isFraud(); - final Long chargeOffReasonCodeValue = accountingBridgeData.getChargeOffReasonCodeValue(); - final boolean isLoanMarkedAsWrittenOff = accountingBridgeData.isWrittenOff(); - final boolean cashBasedAccountingEnabled = accountingBridgeData.isCashBasedAccountingEnabled(); - final boolean upfrontAccrualBasedAccountingEnabled = accountingBridgeData.isUpfrontAccrualBasedAccountingEnabled(); - final boolean periodicAccrualBasedAccountingEnabled = accountingBridgeData.isPeriodicAccrualBasedAccountingEnabled(); - final boolean merchantBuyDownFee = accountingBridgeData.isMerchantBuyDownFee(); - - final List loanTransactionDTOs = accountingBridgeData.getNewLoanTransactions(); - - for (final AccountingBridgeLoanTransactionDTO loanTxnDto : loanTransactionDTOs) { - final Long transactionOfficeId = loanTxnDto.getOfficeId(); - final String transactionId = loanTxnDto.getId().toString(); - final LocalDate transactionDate = loanTxnDto.getDate(); - final LoanTransactionEnumData transactionType = loanTxnDto.getType(); - final BigDecimal amount = loanTxnDto.getAmount(); - final BigDecimal principal = loanTxnDto.getPrincipalPortion(); - final BigDecimal interest = loanTxnDto.getInterestPortion(); - final BigDecimal fees = loanTxnDto.getFeeChargesPortion(); - final BigDecimal penalties = loanTxnDto.getPenaltyChargesPortion(); - final BigDecimal overPayments = loanTxnDto.getOverPaymentPortion(); - final boolean reversed = loanTxnDto.isReversed(); - final Long paymentTypeId = loanTxnDto.getPaymentTypeId(); - final String chargeRefundChargeType = loanTxnDto.getChargeRefundChargeType(); - final LoanChargeData loanChargeData = loanTxnDto.getLoanChargeData(); - - final List feePaymentDetails = new ArrayList<>(); - final List penaltyPaymentDetails = new ArrayList<>(); - // extract charge payment details (if exists) - if (loanTxnDto.getLoanChargesPaid() != null) { - List loanChargesPaidData = loanTxnDto.getLoanChargesPaid(); - for (final LoanChargePaidByDTO loanChargePaid : loanChargesPaidData) { - final Long chargeId = loanChargePaid.getChargeId(); - final Long loanChargeId = loanChargePaid.getLoanChargeId(); - final boolean isPenalty = loanChargePaid.getIsPenalty(); - final BigDecimal chargeAmountPaid = loanChargePaid.getAmount(); - final ChargePaymentDTO chargePaymentDTO = new ChargePaymentDTO(chargeId, chargeAmountPaid, loanChargeId); - if (isPenalty) { - penaltyPaymentDetails.add(chargePaymentDTO); - } else { - feePaymentDetails.add(chargePaymentDTO); - } - } - } - - boolean localIsAccountTransfer = isAccountTransfer; - if (!localIsAccountTransfer) { - localIsAccountTransfer = this.accountTransfersReadPlatformService.isAccountTransfer(Long.parseLong(transactionId), - PortfolioAccountType.LOAN); - } - - BigDecimal principalPaid = loanTxnDto.getPrincipalPaid(); - BigDecimal feePaid = loanTxnDto.getFeePaid(); - BigDecimal penaltyPaid = loanTxnDto.getPenaltyPaid(); - - final LoanTransactionDTO transaction = new LoanTransactionDTO(transactionOfficeId, paymentTypeId, transactionId, - transactionDate, transactionType, amount, principal, interest, fees, penalties, overPayments, reversed, - penaltyPaymentDetails, feePaymentDetails, localIsAccountTransfer, chargeRefundChargeType, loanChargeData, principalPaid, - feePaid, penaltyPaid); - - transaction.setLoanToLoanTransfer(loanTxnDto.isLoanToLoanTransfer()); - newLoanTransactions.add(transaction); - } - - return new LoanDTO(loanId, loanProductId, officeId, currencyCode, cashBasedAccountingEnabled, upfrontAccrualBasedAccountingEnabled, - periodicAccrualBasedAccountingEnabled, newLoanTransactions, isLoanMarkedAsChargeOff, isLoanMarkedAsFraud, - chargeOffReasonCodeValue, isLoanMarkedAsWrittenOff, merchantBuyDownFee, - accountingBridgeData.getBuydownFeeClassificationCodeValue(), - accountingBridgeData.getCapitalizedIncomeClassificationCodeValue(), accountingBridgeData.getWriteOffReasonCodeValue()); - } - - public ProductToGLAccountMapping getChargeOffMappingByCodeValue(Long loanProductId, PortfolioProductType productType, - Long chargeOffReasonId) { - return accountMappingRepository.findChargeOffReasonMapping(loanProductId, productType.getValue(), chargeOffReasonId); - } - - public ProductToGLAccountMapping getWriteOffMappingByCodeValue(Long loanProductId, PortfolioProductType productType, - Long writeOffReasonId) { - return accountMappingRepository.findWriteOffReasonMapping(loanProductId, productType.getValue(), writeOffReasonId); - } - - public ProductToGLAccountMapping getClassificationMappingByCodeValue(Long loanProductId, PortfolioProductType productType, - final Long classificationId, final String classificationType) { - if (LoanProductAccountingParams.BUYDOWN_FEE_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS.getValue().equals(classificationType)) { - return accountMappingRepository.findBuydownFeeClassificationMapping(loanProductId, productType.getValue(), classificationId); - } else { - return accountMappingRepository.findCapitalizedIncomeClassificationMapping(loanProductId, productType.getValue(), - classificationId); - } - } - - public SavingsDTO populateSavingsDtoFromMap(final Map accountingBridgeData, final boolean cashBasedAccountingEnabled, - final boolean accrualBasedAccountingEnabled) { - final Long loanId = (Long) accountingBridgeData.get("savingsId"); - final Long loanProductId = (Long) accountingBridgeData.get("savingsProductId"); - final Long officeId = (Long) accountingBridgeData.get("officeId"); - final String currencyCode = (String) accountingBridgeData.get("currencyCode"); - final List newSavingsTransactions = new ArrayList<>(); - boolean isAccountTransfer = (Boolean) accountingBridgeData.get("isAccountTransfer"); - - @SuppressWarnings("unchecked") - final List> newTransactionsMap = (List>) accountingBridgeData.get("newSavingsTransactions"); - - for (final Map map : newTransactionsMap) { - final Long transactionOfficeId = (Long) map.get("officeId"); - final String transactionId = ((Long) map.get("id")).toString(); - final LocalDate transactionDate = ((LocalDate) map.get("date")); - final SavingsAccountTransactionEnumData transactionType = (SavingsAccountTransactionEnumData) map.get("type"); - final BigDecimal amount = (BigDecimal) map.get("amount"); - final boolean reversed = (Boolean) map.get("reversed"); - final Long paymentTypeId = (Long) map.get("paymentTypeId"); - final BigDecimal overdraftAmount = (BigDecimal) map.get("overdraftAmount"); - - final List feePayments = new ArrayList<>(); - final List penaltyPayments = new ArrayList<>(); - // extract charge payment details (if exists) - if (map.containsKey("savingsChargesPaid")) { - @SuppressWarnings("unchecked") - final List> savingsChargesPaidData = (List>) map.get("savingsChargesPaid"); - for (final Map loanChargePaid : savingsChargesPaidData) { - final Long chargeId = (Long) loanChargePaid.get("chargeId"); - final Long loanChargeId = (Long) loanChargePaid.get("savingsChargeId"); - final boolean isPenalty = (Boolean) loanChargePaid.get("isPenalty"); - final BigDecimal chargeAmountPaid = (BigDecimal) loanChargePaid.get("amount"); - final ChargePaymentDTO chargePaymentDTO = new ChargePaymentDTO(chargeId, chargeAmountPaid, loanChargeId); - if (isPenalty) { - penaltyPayments.add(chargePaymentDTO); - } else { - feePayments.add(chargePaymentDTO); - } - } - } - - final List taxPayments = new ArrayList<>(); - if (map.containsKey("taxDetails")) { - @SuppressWarnings("unchecked") - final List> taxDataList = (List>) map.get("taxDetails"); - for (final Map taxData : taxDataList) { - final BigDecimal taxAmount = (BigDecimal) taxData.get("amount"); - final Long creditAccountId = (Long) taxData.get("creditAccountId"); - final Long debitAccountId = (Long) taxData.get("debitAccountId"); - taxPayments.add(new TaxPaymentDTO(debitAccountId, creditAccountId, taxAmount)); - } - } - - if (!isAccountTransfer) { - isAccountTransfer = this.accountTransfersReadPlatformService.isAccountTransfer(Long.parseLong(transactionId), - PortfolioAccountType.SAVINGS); - } - final SavingsTransactionDTO transaction = new SavingsTransactionDTO(transactionOfficeId, paymentTypeId, transactionId, - transactionDate, transactionType, amount, reversed, feePayments, penaltyPayments, overdraftAmount, isAccountTransfer, - taxPayments); - - newSavingsTransactions.add(transaction); - - } - - return new SavingsDTO(loanId, loanProductId, officeId, currencyCode, cashBasedAccountingEnabled, accrualBasedAccountingEnabled, - newSavingsTransactions); - } - - public SharesDTO populateSharesDtoFromMap(final Map accountingBridgeData, final boolean cashBasedAccountingEnabled, - final boolean accrualBasedAccountingEnabled) { - final Long shareAccountId = (Long) accountingBridgeData.get("shareAccountId"); - final Long shareProductId = (Long) accountingBridgeData.get("shareProductId"); - final Long officeId = (Long) accountingBridgeData.get("officeId"); - final String currencyCode = (String) accountingBridgeData.get("currencyCode"); - final List newTransactions = new ArrayList<>(); - - @SuppressWarnings("unchecked") - final List> newTransactionsMap = (List>) accountingBridgeData.get("newTransactions"); - - for (final Map map : newTransactionsMap) { - final Long transactionOfficeId = (Long) map.get("officeId"); - final String transactionId = ((Long) map.get("id")).toString(); - final LocalDate transactionDate = ((LocalDate) map.get("date")); - final ShareAccountTransactionEnumData transactionType = (ShareAccountTransactionEnumData) map.get("type"); - final ShareAccountTransactionEnumData transactionStatus = (ShareAccountTransactionEnumData) map.get("status"); - final BigDecimal amount = (BigDecimal) map.get("amount"); - final BigDecimal chargeAmount = (BigDecimal) map.get("chargeAmount"); - final Long paymentTypeId = (Long) map.get("paymentTypeId"); - - final List feePayments = new ArrayList<>(); - // extract charge payment details (if exists) - if (map.containsKey("chargesPaid")) { - @SuppressWarnings("unchecked") - final List> chargesPaidData = (List>) map.get("chargesPaid"); - for (final Map chargePaid : chargesPaidData) { - final Long chargeId = (Long) chargePaid.get("chargeId"); - final Long loanChargeId = (Long) chargePaid.get("sharesChargeId"); - final BigDecimal chargeAmountPaid = (BigDecimal) chargePaid.get("amount"); - final ChargePaymentDTO chargePaymentDTO = new ChargePaymentDTO(chargeId, chargeAmountPaid, loanChargeId); - feePayments.add(chargePaymentDTO); - } - } - final SharesTransactionDTO transaction = new SharesTransactionDTO(transactionOfficeId, paymentTypeId, transactionId, - transactionDate, transactionType, transactionStatus, amount, chargeAmount, feePayments); - - newTransactions.add(transaction); - - } - - return new SharesDTO(shareAccountId, shareProductId, officeId, currencyCode, cashBasedAccountingEnabled, - accrualBasedAccountingEnabled, newTransactions); - } - - public ClientTransactionDTO populateClientTransactionDtoFromMap(final Map accountingBridgeData) { - - final Long transactionOfficeId = (Long) accountingBridgeData.get("officeId"); - final Long clientId = (Long) accountingBridgeData.get("clientId"); - final Long transactionId = (Long) accountingBridgeData.get("id"); - final LocalDate transactionDate = ((LocalDate) accountingBridgeData.get("date")); - final EnumOptionData transactionType = (EnumOptionData) accountingBridgeData.get("type"); - final BigDecimal amount = (BigDecimal) accountingBridgeData.get("amount"); - final boolean reversed = (Boolean) accountingBridgeData.get("reversed"); - final Long paymentTypeId = (Long) accountingBridgeData.get("paymentTypeId"); - final String currencyCode = (String) accountingBridgeData.get("currencyCode"); - final Boolean accountingEnabled = (Boolean) accountingBridgeData.get("accountingEnabled"); - - final List clientChargePaymentDTOs = new ArrayList<>(); - // extract client charge payment details (if exists) - if (accountingBridgeData.containsKey("clientChargesPaid")) { - @SuppressWarnings("unchecked") - final List> clientChargesPaidData = (List>) accountingBridgeData - .get("clientChargesPaid"); - for (final Map clientChargePaid : clientChargesPaidData) { - final Long chargeId = (Long) clientChargePaid.get("chargeId"); - final Long clientChargeId = (Long) clientChargePaid.get("clientChargeId"); - final boolean isPenalty = (Boolean) clientChargePaid.get("isPenalty"); - final BigDecimal chargeAmountPaid = (BigDecimal) clientChargePaid.get("amount"); - final Long incomeAccountId = (Long) clientChargePaid.get("incomeAccountId"); - final ClientChargePaymentDTO clientChargePaymentDTO = new ClientChargePaymentDTO(chargeId, chargeAmountPaid, clientChargeId, - isPenalty, incomeAccountId); - clientChargePaymentDTOs.add(clientChargePaymentDTO); - } - } - - return new ClientTransactionDTO(clientId, transactionOfficeId, paymentTypeId, transactionId, transactionDate, transactionType, - currencyCode, amount, reversed, accountingEnabled, clientChargePaymentDTOs); - - } - - /** - * Convenience method that creates a pair of related Debits and Credits for Accrual Based accounting. - * - * The target accounts for debits and credits are switched in case of a reversal - * - * @param office - * office - * @param currencyCode - * currencyCode - * @param accountTypeToBeDebited - * Enum of the placeholder GLAccount to be debited - * @param accountTypeToBeCredited - * Enum of the placeholder of the GLAccount to be credited - * @param loanProductId - * loanProductId - * @param loanId - * loanId - * @param transactionId - * transactionId - * @param transactionDate - * transactionDate - * @param totalAmount - * totalAmount - * @param chargePaymentDTOs - * chargePaymentDTOs - */ - public void createJournalEntriesForLoanCharges(final Office office, final String currencyCode, final Integer accountTypeToBeDebited, - final Integer accountTypeToBeCredited, final Long loanProductId, final Long loanId, final String transactionId, - final LocalDate transactionDate, final BigDecimal totalAmount, final List chargePaymentDTOs) { - - final Map creditDetailsMap = new LinkedHashMap<>(); - final Map debitDetailsMap = new LinkedHashMap<>(); - - for (final ChargePaymentDTO chargePaymentDTO : chargePaymentDTOs) { - final Long chargeId = chargePaymentDTO.getChargeId(); - final GLAccount chargeSpecificCreditAccount = getLinkedGLAccountForLoanCharges(loanProductId, accountTypeToBeCredited, - chargeId); - final GLAccount chargeSpecificDebitAccount = getLinkedGLAccountForLoanCharges(loanProductId, accountTypeToBeDebited, chargeId); - final BigDecimal chargeSpecificAmount = chargePaymentDTO.getAmount(); - - // aggregate amounts by account for credit entries - creditDetailsMap.merge(chargeSpecificCreditAccount, chargeSpecificAmount, BigDecimal::add); - - // aggregate amounts by account for debit entries - debitDetailsMap.merge(chargeSpecificDebitAccount, chargeSpecificAmount, BigDecimal::add); - } - - BigDecimal totalCreditedAmount = BigDecimal.ZERO; - BigDecimal totalDebitedAmount = BigDecimal.ZERO; - - // Create credit journal entries - for (final Map.Entry entry : creditDetailsMap.entrySet()) { - final GLAccount account = entry.getKey(); - final BigDecimal amount = entry.getValue(); - totalCreditedAmount = totalCreditedAmount.add(amount); - createCreditJournalEntryForLoan(office, currencyCode, account, loanId, transactionId, transactionDate, amount); - } - - // Create debit journal entries using charge-specific debit accounts - for (final Map.Entry entry : debitDetailsMap.entrySet()) { - final GLAccount account = entry.getKey(); - final BigDecimal amount = entry.getValue(); - totalDebitedAmount = totalDebitedAmount.add(amount); - createDebitJournalEntryForLoan(office, currencyCode, account, loanId, transactionId, transactionDate, amount); - } - - if (totalAmount.compareTo(totalCreditedAmount) != 0) { - throw new PlatformDataIntegrityException( - "Meltdown in advanced accounting...sum of all charge credits does not equal the total transaction amount", - "Sum of charge credits (" + totalCreditedAmount + ") does not equal transaction total (" + totalAmount + ") for loan " - + loanId + ", transaction " + transactionId, - totalCreditedAmount, totalAmount); - } - - if (totalAmount.compareTo(totalDebitedAmount) != 0) { - throw new PlatformDataIntegrityException( - "Meltdown in advanced accounting...sum of all charge debits does not equal the total transaction amount", - "Sum of charge debits (" + totalDebitedAmount + ") does not equal transaction total (" + totalAmount + ") for loan " - + loanId + ", transaction " + transactionId, - totalDebitedAmount, totalAmount); - } - } - - /** - * Convenience method that creates a pair of related Debits and Credits for Cash Based accounting. - * - * The target accounts for debits and credits are switched in case of a reversal - * - * @param office - * @param accountTypeToBeDebited - * Enum of the placeholder GLAccount to be debited - * @param accountTypeToBeCredited - * Enum of the placeholder of the GLAccount to be credited - * @param savingsProductId - * @param paymentTypeId - * @param loanId - * @param transactionId - * @param transactionDate - * @param amount - * @param isReversal - */ - public void createCashBasedJournalEntriesAndReversalsForSavings(final Office office, final String currencyCode, - final Integer accountTypeToBeDebited, final Integer accountTypeToBeCredited, final Long savingsProductId, - final Long paymentTypeId, final Long loanId, final String transactionId, final LocalDate transactionDate, - final BigDecimal amount, final Boolean isReversal) { - int accountTypeToDebitId = accountTypeToBeDebited; - int accountTypeToCreditId = accountTypeToBeCredited; - // reverse debits and credits for reversals - if (isReversal) { - accountTypeToDebitId = accountTypeToBeCredited; - accountTypeToCreditId = accountTypeToBeDebited; - } - createJournalEntriesForSavings(office, currencyCode, accountTypeToDebitId, accountTypeToCreditId, savingsProductId, paymentTypeId, - loanId, transactionId, transactionDate, amount); - } - - /** - * Convenience method that creates a pair of related Debits and Credits for Cash Based accounting. - * - * The target accounts for debits and credits are switched in case of a reversal - * - * @param office - * @param accountTypeToBeDebited - * Enum of the placeholder GLAccount to be debited - * @param accountTypeToBeCredited - * Enum of the placeholder of the GLAccount to be credited - * @param loanProductId - * @param paymentTypeId - * @param loanId - * @param transactionId - * @param transactionDate - * @param amount - */ - public void createJournalEntriesForLoan(final Office office, final String currencyCode, final Integer accountTypeToBeDebited, - final Integer accountTypeToBeCredited, final Long loanProductId, final Long paymentTypeId, final Long loanId, - final String transactionId, final LocalDate transactionDate, final BigDecimal amount) { - int accountTypeToDebitId = accountTypeToBeDebited; - int accountTypeToCreditId = accountTypeToBeCredited; - createJournalEntriesForLoan(office, currencyCode, accountTypeToDebitId, accountTypeToCreditId, loanProductId, paymentTypeId, loanId, - transactionId, transactionDate, amount); - } - - public void createJournalEntriesForLoan(final Office office, final String currencyCode, final Integer accountTypeToBeDebited, - final GLAccount accountToBeCredited, final Long loanProductId, final Long paymentTypeId, final Long loanId, - final String transactionId, final LocalDate transactionDate, final BigDecimal amount) { - int accountTypeToDebitId = accountTypeToBeDebited; - createJournalEntriesForLoan(office, currencyCode, accountTypeToDebitId, accountToBeCredited, loanProductId, paymentTypeId, loanId, - transactionId, transactionDate, amount); - } - - public void createSplitJournalEntriesForLoan(Office office, String currencyCode, List splitAccountsHolder, - JournalAmountHolder totalAccountHolder, Long loanProductId, Long paymentTypeId, Long loanId, String transactionId, - LocalDate transactionDate) { - splitAccountsHolder.forEach(journalItemHolder -> { - if (MathUtil.isGreaterThanZero(journalItemHolder.getAmount())) { - final GLAccount account = getLinkedGLAccountForLoanProduct(loanProductId, journalItemHolder.getAccountType(), - paymentTypeId); - createDebitJournalEntryForLoan(office, currencyCode, account, loanId, transactionId, transactionDate, - journalItemHolder.getAmount()); - } - }); - if (MathUtil.isGreaterThanZero(totalAccountHolder.getAmount())) { - final GLAccount totalAccount = getLinkedGLAccountForLoanProduct(loanProductId, totalAccountHolder.getAccountType(), - paymentTypeId); - createCreditJournalEntryForLoan(office, currencyCode, totalAccount, loanId, transactionId, transactionDate, - totalAccountHolder.getAmount()); - } - } - - public void createCreditJournalEntryForLoan(final Office office, final String currencyCode, - final CashAccountsForLoan accountMappingType, final Long loanProductId, final Long paymentTypeId, final Long loanId, - final String transactionId, final LocalDate transactionDate, final BigDecimal amount) { - final int accountMappingTypeId = accountMappingType.getValue(); - createCreditJournalEntryForLoan(office, currencyCode, accountMappingTypeId, loanProductId, paymentTypeId, loanId, transactionId, - transactionDate, amount); - } - - public void createCreditJournalEntryForLoan(final Office office, final String currencyCode, - final AccrualAccountsForLoan accountMappingType, final Long loanProductId, final Long paymentTypeId, final Long loanId, - final String transactionId, final LocalDate transactionDate, final BigDecimal amount) { - final int accountMappingTypeId = accountMappingType.getValue(); - createCreditJournalEntryForLoan(office, currencyCode, accountMappingTypeId, loanProductId, paymentTypeId, loanId, transactionId, - transactionDate, amount); - } - - /** - * @param latestGLClosure - * @param transactionDate - */ - public void checkForBranchClosures(final GLClosure latestGLClosure, final LocalDate transactionDate) { - // check if an accounting closure has happened for this branch after the transaction Date - if (latestGLClosure != null) { - if (!DateUtils.isBefore(latestGLClosure.getClosingDate(), transactionDate)) { - throw new JournalEntryInvalidException(GlJournalEntryInvalidReason.ACCOUNTING_CLOSED, latestGLClosure.getClosingDate(), - null, null); - } - } - } - - public GLClosure getLatestClosureByBranch(final long officeId) { - return this.closureRepository.getLatestGLClosureByBranch(officeId); - } - - private void createJournalEntriesForLoan(final Office office, final String currencyCode, final int accountTypeToDebitId, - final int accountTypeToCreditId, final Long loanProductId, final Long paymentTypeId, final Long loanId, - final String transactionId, final LocalDate transactionDate, final BigDecimal amount) { - final GLAccount debitAccount = getLinkedGLAccountForLoanProduct(loanProductId, accountTypeToDebitId, paymentTypeId); - final GLAccount creditAccount = getLinkedGLAccountForLoanProduct(loanProductId, accountTypeToCreditId, paymentTypeId); - createDebitJournalEntryForLoan(office, currencyCode, debitAccount, loanId, transactionId, transactionDate, amount); - createCreditJournalEntryForLoan(office, currencyCode, creditAccount, loanId, transactionId, transactionDate, amount); - } - - private void createJournalEntriesForLoan(final Office office, final String currencyCode, final int accountTypeToDebitId, - final GLAccount creditAccount, final Long loanProductId, final Long paymentTypeId, final Long loanId, - final String transactionId, final LocalDate transactionDate, final BigDecimal amount) { - final GLAccount debitAccount = getLinkedGLAccountForLoanProduct(loanProductId, accountTypeToDebitId, paymentTypeId); - createDebitJournalEntryForLoan(office, currencyCode, debitAccount, loanId, transactionId, transactionDate, amount); - createCreditJournalEntryForLoan(office, currencyCode, creditAccount, loanId, transactionId, transactionDate, amount); - } - - private void createJournalEntriesForSavings(final Office office, final String currencyCode, final int accountTypeToDebitId, - final int accountTypeToCreditId, final Long savingsProductId, final Long paymentTypeId, final Long savingsId, - final String transactionId, final LocalDate transactionDate, final BigDecimal amount) { - final GLAccount debitAccount = getLinkedGLAccountForSavingsProduct(savingsProductId, accountTypeToDebitId, paymentTypeId); - final GLAccount creditAccount = getLinkedGLAccountForSavingsProduct(savingsProductId, accountTypeToCreditId, paymentTypeId); - createDebitJournalEntryForSavings(office, currencyCode, debitAccount, savingsId, transactionId, transactionDate, amount); - createCreditJournalEntryForSavings(office, currencyCode, creditAccount, savingsId, transactionId, transactionDate, amount); - } - - /** - * Convenience method that creates a pair of related Debits and Credits for Cash Based accounting. - * - * The target accounts for debits and credits are switched in case of a reversal - * - * @param office - * @param currencyCode - * @param accountTypeToBeDebited - * Enum of the placeholder GLAccount to be debited - * @param accountTypeToBeCredited - * Enum of the placeholder of the GLAccount to be credited - * @param savingsProductId - * @param paymentTypeId - * @param savingsId - * @param transactionId - * @param transactionDate - * @param amount - * @param isReversal - * @param taxDetails - */ - public void createCashBasedJournalEntriesAndReversalsForSavingsTax(final Office office, final String currencyCode, - final CashAccountsForSavings accountTypeToBeDebited, final CashAccountsForSavings accountTypeToBeCredited, - final Long savingsProductId, final Long paymentTypeId, final Long savingsId, final String transactionId, - final LocalDate transactionDate, final BigDecimal amount, final Boolean isReversal, final List taxDetails) { - - for (TaxPaymentDTO taxPaymentDTO : taxDetails) { - if (taxPaymentDTO.getAmount() != null) { - if (taxPaymentDTO.getCreditAccountId() == null) { - createCashBasedCreditJournalEntriesAndReversalsForSavings(office, currencyCode, accountTypeToBeCredited.getValue(), - savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, taxPaymentDTO.getAmount(), - isReversal); - } else { - createCashBasedCreditJournalEntriesAndReversalsForSavings(office, currencyCode, taxPaymentDTO.getCreditAccountId(), - savingsId, transactionId, transactionDate, taxPaymentDTO.getAmount(), isReversal); - } - } - } - createCashBasedDebitJournalEntriesAndReversalsForSavings(office, currencyCode, accountTypeToBeDebited.getValue(), savingsProductId, - paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); - } - - public void createAccrualBasedJournalEntriesAndReversalsForSavingsTax(final Office office, final String currencyCode, - final AccountingConstants.AccrualAccountsForSavings accountTypeToBeDebited, - final AccountingConstants.AccrualAccountsForSavings accountTypeToBeCredited, final Long savingsProductId, - final Long paymentTypeId, final Long savingsId, final String transactionId, final LocalDate transactionDate, - final BigDecimal amount, final Boolean isReversal, final List taxDetails) { - for (TaxPaymentDTO taxPaymentDTO : taxDetails) { - if (taxPaymentDTO.getAmount() != null) { - if (taxPaymentDTO.getCreditAccountId() == null) { - createAccrualBasedCreditJournalEntriesAndReversalsForSavings(office, currencyCode, accountTypeToBeCredited.getValue(), - savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, taxPaymentDTO.getAmount(), - isReversal); - } else { - createAccrualBasedBasedCreditJournalEntriesAndReversalsForSavings(office, currencyCode, - taxPaymentDTO.getCreditAccountId(), savingsId, transactionId, transactionDate, taxPaymentDTO.getAmount(), - isReversal); - } - } - } - createAccrualBasedDebitJournalEntriesAndReversalsForSavings(office, currencyCode, accountTypeToBeDebited.getValue(), - savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); - } - - public void createCashBasedDebitJournalEntriesAndReversalsForSavings(final Office office, final String currencyCode, - final Integer accountTypeToBeDebited, final Long savingsProductId, final Long paymentTypeId, final Long savingsId, - final String transactionId, final LocalDate transactionDate, final BigDecimal amount, final Boolean isReversal) { - // reverse debits and credits for reversals - if (isReversal) { - createCreditJournalEntriesForSavings(office, currencyCode, accountTypeToBeDebited, savingsProductId, paymentTypeId, savingsId, - transactionId, transactionDate, amount); - } else { - createDebitJournalEntriesForSavings(office, currencyCode, accountTypeToBeDebited, savingsProductId, paymentTypeId, savingsId, - transactionId, transactionDate, amount); - } - } - - public void createCashBasedCreditJournalEntriesAndReversalsForSavings(final Office office, final String currencyCode, - final Integer accountTypeToBeCredited, final Long savingsProductId, final Long paymentTypeId, final Long savingsId, - final String transactionId, final LocalDate transactionDate, final BigDecimal amount, final Boolean isReversal) { - // reverse debits and credits for reversals - if (isReversal) { - createDebitJournalEntriesForSavings(office, currencyCode, accountTypeToBeCredited, savingsProductId, paymentTypeId, savingsId, - transactionId, transactionDate, amount); - } else { - createCreditJournalEntriesForSavings(office, currencyCode, accountTypeToBeCredited, savingsProductId, paymentTypeId, savingsId, - transactionId, transactionDate, amount); - } - } - - public void createCashBasedCreditJournalEntriesAndReversalsForSavings(final Office office, final String currencyCode, - final Long creditAccountId, final Long savingsId, final String transactionId, final LocalDate transactionDate, - final BigDecimal amount, final Boolean isReversal) { - // reverse debits and credits for reversals - final GLAccount creditAccount = getGLAccountById(creditAccountId); - if (isReversal) { - createDebitJournalEntryForSavings(office, currencyCode, creditAccount, savingsId, transactionId, transactionDate, amount); - } else { - createCreditJournalEntryForSavings(office, currencyCode, creditAccount, savingsId, transactionId, transactionDate, amount); - } - } - - public void createAccrualBasedDebitJournalEntriesAndReversalsForSavings(final Office office, final String currencyCode, - final Integer accountTypeToBeDebited, final Long savingsProductId, final Long paymentTypeId, final Long savingsId, - final String transactionId, final LocalDate transactionDate, final BigDecimal amount, final Boolean isReversal) { - // reverse debits and credits for reversals - if (isReversal) { - createCreditJournalEntriesForSavings(office, currencyCode, accountTypeToBeDebited, savingsProductId, paymentTypeId, savingsId, - transactionId, transactionDate, amount); - } else { - createDebitJournalEntriesForSavings(office, currencyCode, accountTypeToBeDebited, savingsProductId, paymentTypeId, savingsId, - transactionId, transactionDate, amount); - } - } - - public void createAccrualBasedCreditJournalEntriesAndReversalsForSavings(final Office office, final String currencyCode, - final Integer accountTypeToBeCredited, final Long savingsProductId, final Long paymentTypeId, final Long savingsId, - final String transactionId, final LocalDate transactionDate, final BigDecimal amount, final Boolean isReversal) { - // reverse debits and credits for reversals - if (isReversal) { - createDebitJournalEntriesForSavings(office, currencyCode, accountTypeToBeCredited, savingsProductId, paymentTypeId, savingsId, - transactionId, transactionDate, amount); - } else { - createCreditJournalEntriesForSavings(office, currencyCode, accountTypeToBeCredited, savingsProductId, paymentTypeId, savingsId, - transactionId, transactionDate, amount); - } - } - - public void createAccrualBasedBasedCreditJournalEntriesAndReversalsForSavings(final Office office, final String currencyCode, - final Long creditAccountId, final Long savingsId, final String transactionId, final LocalDate transactionDate, - final BigDecimal amount, final Boolean isReversal) { - // reverse debits and credits for reversals - final GLAccount creditAccount = getGLAccountById(creditAccountId); - if (isReversal) { - createDebitJournalEntryForSavings(office, currencyCode, creditAccount, savingsId, transactionId, transactionDate, amount); - } else { - createCreditJournalEntryForSavings(office, currencyCode, creditAccount, savingsId, transactionId, transactionDate, amount); - } - } - - private void createDebitJournalEntriesForSavings(final Office office, final String currencyCode, final int accountTypeToDebitId, - final Long savingsProductId, final Long paymentTypeId, final Long savingsId, final String transactionId, - final LocalDate transactionDate, final BigDecimal amount) { - final GLAccount debitAccount = getLinkedGLAccountForSavingsProduct(savingsProductId, accountTypeToDebitId, paymentTypeId); - createDebitJournalEntryForSavings(office, currencyCode, debitAccount, savingsId, transactionId, transactionDate, amount); - } - - private void createCreditJournalEntriesForSavings(final Office office, final String currencyCode, final int accountTypeToCreditId, - final Long savingsProductId, final Long paymentTypeId, final Long savingsId, final String transactionId, - final LocalDate transactionDate, final BigDecimal amount) { - final GLAccount creditAccount = getLinkedGLAccountForSavingsProduct(savingsProductId, accountTypeToCreditId, paymentTypeId); - createCreditJournalEntryForSavings(office, currencyCode, creditAccount, savingsId, transactionId, transactionDate, amount); - } - - public void createDebitJournalEntryForLoan(final Office office, final String currencyCode, final int accountMappingTypeId, - final Long loanProductId, final Long paymentTypeId, final Long loanId, final String transactionId, - final LocalDate transactionDate, final BigDecimal amount) { - final GLAccount account = getLinkedGLAccountForLoanProduct(loanProductId, accountMappingTypeId, paymentTypeId); - createDebitJournalEntryForLoan(office, currencyCode, account, loanId, transactionId, transactionDate, amount); - } - - public void createDebitJournalEntryForLoan(final Office office, final String currencyCode, final Long loanId, - final String transactionId, final LocalDate transactionDate, final BigDecimal amount, final GLAccount account) { - createDebitJournalEntryForLoan(office, currencyCode, account, loanId, transactionId, transactionDate, amount); - } - - public void createDebitJournalEntryForLoanCharges(final Office office, final String currencyCode, final int accountMappingTypeId, - final Long loanProductId, final Long chargeId, final Long loanId, final String transactionId, final LocalDate transactionDate, - final BigDecimal amount) { - final GLAccount account = getLinkedGLAccountForLoanCharges(loanProductId, accountMappingTypeId, chargeId); - createDebitJournalEntryForLoan(office, currencyCode, account, loanId, transactionId, transactionDate, amount); - } - - public void createCreditJournalEntryForLoanCharges(final Office office, final String currencyCode, final int accountMappingTypeId, - final Long loanProductId, final Long loanId, final String transactionId, final LocalDate transactionDate, - final BigDecimal totalAmount, final List chargePaymentDTOs) { - createJournalEntriesForLoanChargesInternal(office, currencyCode, accountMappingTypeId, loanProductId, loanId, transactionId, - transactionDate, totalAmount, chargePaymentDTOs, true); - } - - public void createDebitJournalEntryForLoanCharges(final Office office, final String currencyCode, final int accountMappingTypeId, - final Long loanProductId, final Long loanId, final String transactionId, final LocalDate transactionDate, - final BigDecimal totalAmount, final List chargePaymentDTOs) { - createJournalEntriesForLoanChargesInternal(office, currencyCode, accountMappingTypeId, loanProductId, loanId, transactionId, - transactionDate, totalAmount, chargePaymentDTOs, false); - } - - /** - * Convenience method that creates a pair of related Debits and Credits for Cash Based accounting. - * - * The target accounts for debits and credits are switched in case of a reversal - * - * @param office - * office - * @param currencyCode - * currencyCode - * @param accountTypeToBeDebited - * Enum of the placeholder GLAccount to be debited - * @param accountTypeToBeCredited - * Enum of the placeholder of the GLAccount to be credited - * @param savingsProductId - * savingsProductId - * @param paymentTypeId - * paymentTypeId - * @param loanId - * loanId - * @param transactionId - * transactionId - * @param transactionDate - * transactionDate - * @param totalAmount - * totalAmount - * @param isReversal - * isReversal - * @param chargePaymentDTOs - * chargePaymentDTOs - */ - public void createCashBasedJournalEntriesAndReversalsForSavingsCharges(final Office office, final String currencyCode, - final CashAccountsForSavings accountTypeToBeDebited, CashAccountsForSavings accountTypeToBeCredited, - final Long savingsProductId, final Long paymentTypeId, final Long loanId, final String transactionId, - final LocalDate transactionDate, final BigDecimal totalAmount, final Boolean isReversal, - final List chargePaymentDTOs) { - // TODO Vishwas: Remove this validation, as and when appropriate Junit - // tests are written for accounting - /** - * Accounting module currently supports a single charge per transaction, throw an error if this is not the case - * here so any developers changing the expected portfolio behavior would also take care of modifying the - * accounting code appropriately - **/ - if (chargePaymentDTOs.size() != 1) { - throw new PlatformDataIntegrityException("Recent Portfolio changes w.r.t Charges for Savings have Broken the accounting code", - "Recent Portfolio changes w.r.t Charges for Savings have Broken the accounting code"); - } - ChargePaymentDTO chargePaymentDTO = chargePaymentDTOs.get(0); - GLAccount chargeSpecificAccount = getLinkedGLAccountForSavingsCharges(savingsProductId, accountTypeToBeCredited.getValue(), - chargePaymentDTO.getChargeId()); - - final GLAccount savingsControlAccount = getLinkedGLAccountForSavingsProduct(savingsProductId, accountTypeToBeDebited.getValue(), - paymentTypeId); - if (isReversal) { - createDebitJournalEntryForSavings(office, currencyCode, chargeSpecificAccount, loanId, transactionId, transactionDate, - totalAmount); - createCreditJournalEntryForSavings(office, currencyCode, savingsControlAccount, loanId, transactionId, transactionDate, - totalAmount); - } else { - createDebitJournalEntryForSavings(office, currencyCode, savingsControlAccount, loanId, transactionId, transactionDate, - totalAmount); - createCreditJournalEntryForSavings(office, currencyCode, chargeSpecificAccount, loanId, transactionId, transactionDate, - totalAmount); - } - } - - public void createAccrualBasedJournalEntriesAndReversalsForSavingsCharges(final Office office, final String currencyCode, - final AccountingConstants.AccrualAccountsForSavings accountTypeToBeDebited, - final AccountingConstants.AccrualAccountsForSavings accountTypeToBeCredited, final Long savingsProductId, - final Long paymentTypeId, final Long loanId, final String transactionId, final LocalDate transactionDate, - final BigDecimal totalAmount, final Boolean isReversal, final List chargePaymentDTOs) { - // TODO Vishwas: Remove this validation, as and when appropriate Junit - // tests are written for accounting - /** - * Accounting module currently supports a single charge per transaction, throw an error if this is not the case - * here so any developers changing the expected portfolio behavior would also take care of modifying the - * accounting code appropriately - **/ - if (chargePaymentDTOs.size() != 1) { - throw new PlatformDataIntegrityException("Recent Portfolio changes w.r.t Charges for Savings have Broken the accounting code", - "Recent Portfolio changes w.r.t Charges for Savings have Broken the accounting code"); - } - ChargePaymentDTO chargePaymentDTO = chargePaymentDTOs.get(0); - GLAccount chargeSpecificAccount = getLinkedGLAccountForSavingsCharges(savingsProductId, accountTypeToBeCredited.getValue(), - chargePaymentDTO.getChargeId()); - - final GLAccount savingsControlAccount = getLinkedGLAccountForSavingsProduct(savingsProductId, accountTypeToBeDebited.getValue(), - paymentTypeId); - if (isReversal) { - createDebitJournalEntryForSavings(office, currencyCode, chargeSpecificAccount, loanId, transactionId, transactionDate, - totalAmount); - createCreditJournalEntryForSavings(office, currencyCode, savingsControlAccount, loanId, transactionId, transactionDate, - totalAmount); - } else { - createDebitJournalEntryForSavings(office, currencyCode, savingsControlAccount, loanId, transactionId, transactionDate, - totalAmount); - createCreditJournalEntryForSavings(office, currencyCode, chargeSpecificAccount, loanId, transactionId, transactionDate, - totalAmount); - } - } - - public Office getOfficeById(final long officeId) { - return this.officeRepository.getReferenceById(officeId); - } - - public void createCreditJournalEntryForLoan(final Office office, final String currencyCode, final int accountMappingTypeId, - final Long loanProductId, final Long paymentTypeId, final Long loanId, final String transactionId, - final LocalDate transactionDate, final BigDecimal amount) { - final GLAccount account = getLinkedGLAccountForLoanProduct(loanProductId, accountMappingTypeId, paymentTypeId); - createCreditJournalEntryForLoan(office, currencyCode, loanId, transactionId, transactionDate, amount, account); - } - - public void createCreditJournalEntryForLoan(final Office office, final String currencyCode, final Long loanId, - final String transactionId, final LocalDate transactionDate, final BigDecimal amount, final GLAccount account) { - createCreditJournalEntryForLoan(office, currencyCode, account, loanId, transactionId, transactionDate, amount); - } - - private void createCreditJournalEntryForClientPayments(final Office office, final String currencyCode, final GLAccount account, - final Long clientId, final Long transactionId, final LocalDate transactionDate, final BigDecimal amount) { - final boolean manualEntry = false; - - String modifiedTransactionId = CLIENT_TRANSACTION_IDENTIFIER + transactionId; - final JournalEntry journalEntry = JournalEntry.createNew(office, null, account, currencyCode, modifiedTransactionId, manualEntry, - transactionDate, JournalEntryType.CREDIT, amount, null, PortfolioProductType.CLIENT.getValue(), clientId, null, null, null, - transactionId, null); - persistJournalEntry(journalEntry); - } - - private void createCreditJournalEntryForSavings(final Office office, final String currencyCode, final GLAccount account, - final Long savingsId, final String transactionId, final LocalDate transactionDate, final BigDecimal amount) - throws DataAccessException { - final boolean manualEntry = false; - Long savingsAccountTransactionId = null; - String modifiedTransactionId = transactionId; - if (StringUtils.isNumeric(transactionId)) { - savingsAccountTransactionId = Long.parseLong(transactionId); - modifiedTransactionId = SAVINGS_TRANSACTION_IDENTIFIER + transactionId; - } - final JournalEntry journalEntry = JournalEntry.createNew(office, null, account, currencyCode, modifiedTransactionId, manualEntry, - transactionDate, JournalEntryType.CREDIT, amount, null, PortfolioProductType.SAVING.getValue(), savingsId, null, null, - savingsAccountTransactionId, null, null); - - persistJournalEntry(journalEntry); - } - - private void createCreditJournalEntryForLoan(final Office office, final String currencyCode, final GLAccount account, final Long loanId, - final String transactionId, final LocalDate transactionDate, final BigDecimal amount) { - final boolean manualEntry = false; - Long loanTransactionId = null; - String modifiedTransactionId = transactionId; - if (StringUtils.isNumeric(transactionId)) { - loanTransactionId = Long.parseLong(transactionId); - modifiedTransactionId = LOAN_TRANSACTION_IDENTIFIER + transactionId; - } - final JournalEntry journalEntry = JournalEntry.createNew(office, null, account, currencyCode, modifiedTransactionId, manualEntry, - transactionDate, JournalEntryType.CREDIT, amount, null, PortfolioProductType.LOAN.getValue(), loanId, null, - loanTransactionId, null, null, null); - persistJournalEntry(journalEntry); - } - - public void createProvisioningDebitJournalEntry(LocalDate transactionDate, Long provisioningEntryId, Office office, String currencyCode, - GLAccount account, BigDecimal amount) { - final boolean manualEntry = false; - String modifiedTransactionId = PROVISIONING_TRANSACTION_IDENTIFIER + provisioningEntryId; - final JournalEntry journalEntry = JournalEntry.createNew(office, null, account, currencyCode, modifiedTransactionId, manualEntry, - transactionDate, JournalEntryType.DEBIT, amount, null, PortfolioProductType.PROVISIONING.getValue(), provisioningEntryId, - null, null, null, null, null); - persistJournalEntry(journalEntry); - } - - public void createProvisioningCreditJournalEntry(LocalDate transactionDate, Long provisioningEntryId, Office office, - String currencyCode, GLAccount account, BigDecimal amount) { - final boolean manualEntry = false; - String modifiedTransactionId = PROVISIONING_TRANSACTION_IDENTIFIER + provisioningEntryId; - final JournalEntry journalEntry = JournalEntry.createNew(office, null, account, currencyCode, modifiedTransactionId, manualEntry, - transactionDate, JournalEntryType.CREDIT, amount, null, PortfolioProductType.PROVISIONING.getValue(), provisioningEntryId, - null, null, null, null, null); - persistJournalEntry(journalEntry); - } - - public void createDebitJournalEntryForLoan(final Office office, final String currencyCode, final GLAccount account, final Long loanId, - final String transactionId, final LocalDate transactionDate, final BigDecimal amount) { - final boolean manualEntry = false; - Long loanTransactionId = null; - String modifiedTransactionId = transactionId; - if (StringUtils.isNumeric(transactionId)) { - loanTransactionId = Long.parseLong(transactionId); - modifiedTransactionId = LOAN_TRANSACTION_IDENTIFIER + transactionId; - } - final JournalEntry journalEntry = JournalEntry.createNew(office, null, account, currencyCode, modifiedTransactionId, manualEntry, - transactionDate, JournalEntryType.DEBIT, amount, null, PortfolioProductType.LOAN.getValue(), loanId, null, - loanTransactionId, null, null, null); - persistJournalEntry(journalEntry); - } - - private void createDebitJournalEntryForSavings(final Office office, final String currencyCode, final GLAccount account, - final Long savingsId, final String transactionId, final LocalDate transactionDate, final BigDecimal amount) { - final boolean manualEntry = false; - Long savingsAccountTransactionId = null; - String modifiedTransactionId = transactionId; - if (StringUtils.isNumeric(transactionId)) { - savingsAccountTransactionId = Long.parseLong(transactionId); - modifiedTransactionId = SAVINGS_TRANSACTION_IDENTIFIER + transactionId; - } - final JournalEntry journalEntry = JournalEntry.createNew(office, null, account, currencyCode, modifiedTransactionId, manualEntry, - transactionDate, JournalEntryType.DEBIT, amount, null, PortfolioProductType.SAVING.getValue(), savingsId, null, null, - savingsAccountTransactionId, null, null); - - persistJournalEntry(journalEntry); - } - - private void createDebitJournalEntryForClientPayments(final Office office, final String currencyCode, final GLAccount account, - final Long clientId, final Long transactionId, final LocalDate transactionDate, final BigDecimal amount) { - final boolean manualEntry = false; - String modifiedTransactionId = CLIENT_TRANSACTION_IDENTIFIER + transactionId; - final JournalEntry journalEntry = JournalEntry.createNew(office, null, account, currencyCode, modifiedTransactionId, manualEntry, - transactionDate, JournalEntryType.DEBIT, amount, null, PortfolioProductType.CLIENT.getValue(), clientId, null, null, null, - transactionId, null); - persistJournalEntry(journalEntry); - } - - public void createJournalEntriesForShares(final Office office, final String currencyCode, final int accountTypeToDebitId, - final int accountTypeToCreditId, final Long shareProductId, final Long paymentTypeId, final Long shareAccountId, - final String transactionId, final LocalDate transactionDate, final BigDecimal amount) { - createDebitJournalEntryForShares(office, currencyCode, accountTypeToDebitId, shareProductId, paymentTypeId, shareAccountId, - transactionId, transactionDate, amount); - createCreditJournalEntryForShares(office, currencyCode, accountTypeToCreditId, shareProductId, paymentTypeId, shareAccountId, - transactionId, transactionDate, amount); - } - - public void createDebitJournalEntryForShares(final Office office, final String currencyCode, final int accountTypeToDebitId, - final Long shareProductId, final Long paymentTypeId, final Long shareAccountId, final String transactionId, - final LocalDate transactionDate, final BigDecimal amount) { - final GLAccount debitAccount = getLinkedGLAccountForShareProduct(shareProductId, accountTypeToDebitId, paymentTypeId); - createDebitJournalEntryForShares(office, currencyCode, debitAccount, shareAccountId, transactionId, transactionDate, amount); - } - - public void createCreditJournalEntryForShares(final Office office, final String currencyCode, final int accountTypeToCreditId, - final Long shareProductId, final Long paymentTypeId, final Long shareAccountId, final String transactionId, - final LocalDate transactionDate, final BigDecimal amount) { - final GLAccount creditAccount = getLinkedGLAccountForShareProduct(shareProductId, accountTypeToCreditId, paymentTypeId); - createCreditJournalEntryForShares(office, currencyCode, creditAccount, shareAccountId, transactionId, transactionDate, amount); - } - - public void createCashBasedJournalEntriesForSharesCharges(final Office office, final String currencyCode, - final CashAccountsForShares accountTypeToBeDebited, final CashAccountsForShares accountTypeToBeCredited, - final Long shareProductId, final Long paymentTypeId, final Long shareAccountId, final String transactionId, - final LocalDate transactionDate, final BigDecimal totalAmount, final List chargePaymentDTOs) { - - createDebitJournalEntryForShares(office, currencyCode, accountTypeToBeDebited.getValue(), shareProductId, paymentTypeId, - shareAccountId, transactionId, transactionDate, totalAmount); - createCashBasedJournalEntryForSharesCharges(office, currencyCode, accountTypeToBeCredited, shareProductId, shareAccountId, - transactionId, transactionDate, totalAmount, chargePaymentDTOs); - } - - public void createCashBasedJournalEntryForSharesCharges(final Office office, final String currencyCode, - final CashAccountsForShares accountTypeToBeCredited, final Long shareProductId, final Long shareAccountId, - final String transactionId, final LocalDate transactionDate, final BigDecimal totalAmount, - final List chargePaymentDTOs) { - final Map creditDetailsMap = new LinkedHashMap<>(); - for (final ChargePaymentDTO chargePaymentDTO : chargePaymentDTOs) { - final GLAccount chargeSpecificAccount = getLinkedGLAccountForShareCharges(shareProductId, accountTypeToBeCredited.getValue(), - chargePaymentDTO.getChargeId()); - BigDecimal chargeSpecificAmount = chargePaymentDTO.getAmount(); - - // adjust net credit amount if the account is already present in the - // map - if (creditDetailsMap.containsKey(chargeSpecificAccount)) { - final BigDecimal existingAmount = creditDetailsMap.get(chargeSpecificAccount); - chargeSpecificAmount = chargeSpecificAmount.add(existingAmount); - } - creditDetailsMap.put(chargeSpecificAccount, chargeSpecificAmount); - } - - BigDecimal totalCreditedAmount = BigDecimal.ZERO; - for (final Map.Entry entry : creditDetailsMap.entrySet()) { - final GLAccount account = entry.getKey(); - final BigDecimal amount = entry.getValue(); - totalCreditedAmount = totalCreditedAmount.add(amount); - createCreditJournalEntryForShares(office, currencyCode, account, shareAccountId, transactionId, transactionDate, amount); - } - if (totalAmount.compareTo(totalCreditedAmount) != 0) { - throw new PlatformDataIntegrityException("Recent Portfolio changes w.r.t Charges for shares have Broken the accounting code", - "Recent Portfolio changes w.r.t Charges for shares have Broken the accounting code"); - } - } - - public void revertCashBasedJournalEntryForSharesCharges(final Office office, final String currencyCode, - final CashAccountsForShares accountTypeToBeCredited, final Long shareProductId, final Long shareAccountId, - final String transactionId, final LocalDate transactionDate, final BigDecimal totalAmount, - final List chargePaymentDTOs) { - final Map creditDetailsMap = new LinkedHashMap<>(); - for (final ChargePaymentDTO chargePaymentDTO : chargePaymentDTOs) { - final GLAccount chargeSpecificAccount = getLinkedGLAccountForShareCharges(shareProductId, accountTypeToBeCredited.getValue(), - chargePaymentDTO.getChargeId()); - BigDecimal chargeSpecificAmount = chargePaymentDTO.getAmount(); - - // adjust net credit amount if the account is already present in the - // map - if (creditDetailsMap.containsKey(chargeSpecificAccount)) { - final BigDecimal existingAmount = creditDetailsMap.get(chargeSpecificAccount); - chargeSpecificAmount = chargeSpecificAmount.add(existingAmount); - } - creditDetailsMap.put(chargeSpecificAccount, chargeSpecificAmount); - } - - BigDecimal totalCreditedAmount = BigDecimal.ZERO; - for (final Map.Entry entry : creditDetailsMap.entrySet()) { - final GLAccount account = entry.getKey(); - final BigDecimal amount = entry.getValue(); - totalCreditedAmount = totalCreditedAmount.add(amount); - createDebitJournalEntryForShares(office, currencyCode, account, shareAccountId, transactionId, transactionDate, amount); - } - if (totalAmount.compareTo(totalCreditedAmount) != 0) { - throw new PlatformDataIntegrityException("Recent Portfolio changes w.r.t Charges for shares have Broken the accounting code", - "Recent Portfolio changes w.r.t Charges for shares have Broken the accounting code"); - } - } - - private void createDebitJournalEntryForShares(final Office office, final String currencyCode, final GLAccount account, - final Long shareAccountId, final String transactionId, final LocalDate transactionDate, final BigDecimal amount) { - final boolean manualEntry = false; - Long shareTransactionId = null; - String modifiedTransactionId = transactionId; - if (StringUtils.isNumeric(transactionId)) { - shareTransactionId = Long.parseLong(transactionId); - modifiedTransactionId = SHARE_TRANSACTION_IDENTIFIER + transactionId; - } - final JournalEntry journalEntry = JournalEntry.createNew(office, null, account, currencyCode, modifiedTransactionId, manualEntry, - transactionDate, JournalEntryType.DEBIT, amount, null, PortfolioProductType.SHARES.getValue(), shareAccountId, null, null, - null, null, shareTransactionId); - persistJournalEntry(journalEntry); - } - - private void createCreditJournalEntryForShares(final Office office, final String currencyCode, final GLAccount account, - final Long shareAccountId, final String transactionId, final LocalDate transactionDate, final BigDecimal amount) { - final boolean manualEntry = false; - Long shareTransactionId = null; - String modifiedTransactionId = transactionId; - if (StringUtils.isNumeric(transactionId)) { - shareTransactionId = Long.parseLong(transactionId); - modifiedTransactionId = SHARE_TRANSACTION_IDENTIFIER + transactionId; - } - final JournalEntry journalEntry = JournalEntry.createNew(office, null, account, currencyCode, modifiedTransactionId, manualEntry, - transactionDate, JournalEntryType.CREDIT, amount, null, PortfolioProductType.SHARES.getValue(), shareAccountId, null, null, - null, null, shareTransactionId); - persistJournalEntry(journalEntry); - } - - public GLAccount getLinkedGLAccountForLoanProduct(final Long loanProductId, final int accountMappingTypeId, final Long paymentTypeId) { - GLAccount glAccount; - if (isOrganizationAccount(accountMappingTypeId)) { - FinancialActivityAccount financialActivityAccount = this.financialActivityAccountRepository - .findByFinancialActivityTypeWithNotFoundDetection(accountMappingTypeId); - glAccount = financialActivityAccount.getGlAccount(); - } else { - ProductToGLAccountMapping accountMapping = this.accountMappingRepository.findCoreProductToFinAccountMapping(loanProductId, - PortfolioProductType.LOAN.getValue(), accountMappingTypeId); - - /**** - * Get more specific mapping for FUND source accounts (based on payment channels). Note that fund source - * placeholder ID would be same for both cash and accrual accounts - ***/ - if (accountMappingTypeId == CashAccountsForLoan.FUND_SOURCE.getValue()) { - final ProductToGLAccountMapping paymentChannelSpecificAccountMapping = this.accountMappingRepository - .findByProductIdAndProductTypeAndFinancialAccountTypeAndPaymentTypeId(loanProductId, - PortfolioProductType.LOAN.getValue(), accountMappingTypeId, paymentTypeId); - if (paymentChannelSpecificAccountMapping != null) { - accountMapping = paymentChannelSpecificAccountMapping; - } - } - - if (accountMapping == null) { - throw new ProductToGLAccountMappingNotFoundException(PortfolioProductType.LOAN, loanProductId, - AccrualAccountsForLoan.fromInt(accountMappingTypeId).toString()); - - } - glAccount = accountMapping.getGlAccount(); - } - return glAccount; - } - - private GLAccount getLinkedGLAccountForLoanCharges(final Long loanProductId, final int accountMappingTypeId, final Long chargeId) { - ProductToGLAccountMapping accountMapping = this.accountMappingRepository.findCoreProductToFinAccountMapping(loanProductId, - PortfolioProductType.LOAN.getValue(), accountMappingTypeId); - /***** - * Get more specific mappings for Charges and penalties (based on the actual charge /penalty coupled with the - * loan product). Note the income from fees and income from penalties placeholder ID would be the same for both - * cash and accrual based accounts - *****/ - - // Check for charge-specific mappings for all account types (not just income accounts) - // This allows charge-specific GL account mappings for debit accounts as well - if (chargeId != null) { - final ProductToGLAccountMapping chargeSpecificAccountMapping = this.accountMappingRepository - .findProductIdAndProductTypeAndFinancialAccountTypeAndChargeId(loanProductId, PortfolioProductType.LOAN.getValue(), - accountMappingTypeId, chargeId); - if (chargeSpecificAccountMapping != null) { - accountMapping = chargeSpecificAccountMapping; - } - } - return accountMapping.getGlAccount(); - } - - private GLAccount getLinkedGLAccountForSavingsCharges(final Long savingsProductId, final int accountMappingTypeId, - final Long chargeId) { - - ProductToGLAccountMapping accountMapping = this.accountMappingRepository.findCoreProductToFinAccountMapping(savingsProductId, - PortfolioProductType.SAVING.getValue(), accountMappingTypeId); - /***** - * Get more specific mappings for Charges and penalties (based on the actual charge /penalty coupled with the - * loan product). Note the income from fees and income from penalties placeholder ID would be the same for both - * cash and accrual based accounts - *****/ - - // Vishwas TODO: remove this condition as it should always be true - - if (accountMappingTypeId == CashAccountsForSavings.INCOME_FROM_FEES.getValue() - || accountMappingTypeId == CashAccountsForLoan.INCOME_FROM_PENALTIES.getValue()) { - GLAccount glAccount = chargeRepositoryWrapper.findOneWithNotFoundDetection(chargeId).getAccount(); - if (glAccount != null) { - return glAccount; - } - final ProductToGLAccountMapping chargeSpecificIncomeAccountMapping = this.accountMappingRepository - .findProductIdAndProductTypeAndFinancialAccountTypeAndChargeId(savingsProductId, PortfolioProductType.SAVING.getValue(), - accountMappingTypeId, chargeId); - if (chargeSpecificIncomeAccountMapping != null) { - - accountMapping = chargeSpecificIncomeAccountMapping; - } - } - - return accountMapping.getGlAccount(); - } - - private GLAccount getLinkedGLAccountForSavingsProduct(final Long savingsProductId, final int accountMappingTypeId, - final Long paymentTypeId) { - GLAccount glAccount; - if (isOrganizationAccount(accountMappingTypeId)) { - FinancialActivityAccount financialActivityAccount = this.financialActivityAccountRepository - .findByFinancialActivityTypeWithNotFoundDetection(accountMappingTypeId); - glAccount = financialActivityAccount.getGlAccount(); - } else { - ProductToGLAccountMapping accountMapping = this.accountMappingRepository.findCoreProductToFinAccountMapping(savingsProductId, - PortfolioProductType.SAVING.getValue(), accountMappingTypeId); - /**** - * Get more specific mapping for FUND source accounts (based on payment channels). Note that fund source - * placeholder ID would be same for both cash and accrual accounts - ***/ - if (accountMappingTypeId == CashAccountsForSavings.SAVINGS_REFERENCE.getValue()) { - final ProductToGLAccountMapping paymentChannelSpecificAccountMapping = this.accountMappingRepository - .findByProductIdAndProductTypeAndFinancialAccountTypeAndPaymentTypeId(savingsProductId, - PortfolioProductType.SAVING.getValue(), accountMappingTypeId, paymentTypeId); - if (paymentChannelSpecificAccountMapping != null) { - accountMapping = paymentChannelSpecificAccountMapping; - } - } - glAccount = accountMapping.getGlAccount(); - } - return glAccount; - } - - private GLAccount getLinkedGLAccountForShareProduct(final Long shareProductId, final int accountMappingTypeId, - final Long paymentTypeId) { - GLAccount glAccount; - if (isOrganizationAccount(accountMappingTypeId)) { - FinancialActivityAccount financialActivityAccount = this.financialActivityAccountRepository - .findByFinancialActivityTypeWithNotFoundDetection(accountMappingTypeId); - glAccount = financialActivityAccount.getGlAccount(); - } else { - ProductToGLAccountMapping accountMapping = this.accountMappingRepository.findCoreProductToFinAccountMapping(shareProductId, - PortfolioProductType.SHARES.getValue(), accountMappingTypeId); - - if (accountMappingTypeId == CashAccountsForShares.SHARES_REFERENCE.getValue()) { - final ProductToGLAccountMapping paymentChannelSpecificAccountMapping = this.accountMappingRepository - .findByProductIdAndProductTypeAndFinancialAccountTypeAndPaymentTypeId(shareProductId, - PortfolioProductType.SHARES.getValue(), accountMappingTypeId, paymentTypeId); - if (paymentChannelSpecificAccountMapping != null) { - accountMapping = paymentChannelSpecificAccountMapping; - } - } - glAccount = accountMapping.getGlAccount(); - } - return glAccount; - } - - private GLAccount getLinkedGLAccountForShareCharges(final Long shareProductId, final int accountMappingTypeId, final Long chargeId) { - ProductToGLAccountMapping accountMapping = this.accountMappingRepository.findCoreProductToFinAccountMapping(shareProductId, - PortfolioProductType.SHARES.getValue(), accountMappingTypeId); - /***** - * Get more specific mappings for Charges and penalties (based on the actual charge /penalty coupled with the - * loan product). Note the income from fees and income from penalties placeholder ID would be the same for both - * cash and accrual based accounts - *****/ - - final ProductToGLAccountMapping chargeSpecificIncomeAccountMapping = this.accountMappingRepository - .findProductIdAndProductTypeAndFinancialAccountTypeAndChargeId(shareProductId, PortfolioProductType.SHARES.getValue(), - accountMappingTypeId, chargeId); - if (chargeSpecificIncomeAccountMapping != null) { - accountMapping = chargeSpecificIncomeAccountMapping; - } - return accountMapping.getGlAccount(); - } - - private boolean isOrganizationAccount(final int accountMappingTypeId) { - return FinancialActivity.fromInt(accountMappingTypeId) != null; - } - - public BigDecimal createCreditJournalEntryOrReversalForClientPayments(final Office office, final String currencyCode, - final Long clientId, final Long transactionId, final LocalDate transactionDate, final Boolean isReversal, - final List clientChargePaymentDTOs) { - /*** - * Map to track each account affected and the net credit to be made for a particular account - ***/ - final Map creditDetailsMap = new LinkedHashMap<>(); - for (final ClientChargePaymentDTO clientChargePaymentDTO : clientChargePaymentDTOs) { - if (clientChargePaymentDTO.getIncomeAccountId() != null) { - final Long accountId = clientChargePaymentDTO.getIncomeAccountId(); - BigDecimal chargeSpecificAmount = clientChargePaymentDTO.getAmount(); - - // adjust net credit amount if the account is already present in - // the map - if (creditDetailsMap.containsKey(accountId)) { - final BigDecimal existingAmount = creditDetailsMap.get(accountId); - chargeSpecificAmount = chargeSpecificAmount.add(existingAmount); - } - creditDetailsMap.put(accountId, chargeSpecificAmount); - } - } - - BigDecimal totalCreditedAmount = BigDecimal.ZERO; - for (final Map.Entry entry : creditDetailsMap.entrySet()) { - final GLAccount account = getGLAccountById(entry.getKey()); - final BigDecimal amount = entry.getValue(); - totalCreditedAmount = totalCreditedAmount.add(amount); - if (isReversal) { - createDebitJournalEntryForClientPayments(office, currencyCode, account, clientId, transactionId, transactionDate, amount); - } else { - createCreditJournalEntryForClientPayments(office, currencyCode, account, clientId, transactionId, transactionDate, amount); - } - } - return totalCreditedAmount; - } - - public void createDebitJournalEntryOrReversalForClientChargePayments(final Office office, final String currencyCode, - final Long clientId, final Long transactionId, final LocalDate transactionDate, final BigDecimal amount, - final Boolean isReversal) { - final GLAccount account = financialActivityAccountRepository - .findByFinancialActivityTypeWithNotFoundDetection(FinancialActivity.ASSET_FUND_SOURCE.getValue()).getGlAccount(); - if (isReversal) { - createCreditJournalEntryForClientPayments(office, currencyCode, account, clientId, transactionId, transactionDate, amount); - } else { - createDebitJournalEntryForClientPayments(office, currencyCode, account, clientId, transactionId, transactionDate, amount); - } - } - - private GLAccount getGLAccountById(final Long accountId) { - return this.glAccountRepository.getReferenceById(accountId); - } - - public Integer getValueForFeeOrPenaltyIncomeAccount(final String chargeRefundChargeType) { - if (chargeRefundChargeType == null - || !(chargeRefundChargeType.equalsIgnoreCase("P") || chargeRefundChargeType.equalsIgnoreCase("F"))) { - String errorValue; - errorValue = Objects.requireNonNullElse(chargeRefundChargeType, "Null"); - throw new PlatformDataIntegrityException("error.msg.chargeRefundChargeType.can.only.be.P.or.F", - "chargeRefundChargeType can only be P (Penalty) or F(Fee) - Value is: " + errorValue); - } - Integer incomeAccount; - if (chargeRefundChargeType.equalsIgnoreCase("P")) { - incomeAccount = AccrualAccountsForLoan.INCOME_FROM_PENALTIES.getValue(); - } else { - incomeAccount = AccrualAccountsForLoan.INCOME_FROM_FEES.getValue(); - - } - return incomeAccount; - } - - public JournalEntry persistJournalEntry(JournalEntry journalEntry) { - boolean isNew = journalEntry.isNew(); - JournalEntry savedJournalEntry = this.glJournalEntryRepository.saveAndFlush(journalEntry); - if (isNew && journalEntry.getLoanTransactionId() != null) { - businessEventNotifierService.notifyPostBusinessEvent(new LoanJournalEntryCreatedBusinessEvent(savedJournalEntry)); - } - return savedJournalEntry; - } - - private void createJournalEntriesForLoanChargesInternal(final Office office, final String currencyCode, final int accountMappingTypeId, - final Long loanProductId, final Long loanId, final String transactionId, final LocalDate transactionDate, - final BigDecimal totalAmount, final List chargePaymentDTOs, final boolean isCredit) { - final Map creditDetailsMap = new LinkedHashMap<>(); - - for (final ChargePaymentDTO chargePaymentDTO : chargePaymentDTOs) { - final Long chargeId = chargePaymentDTO.getChargeId(); - final GLAccount account = getLinkedGLAccountForLoanCharges(loanProductId, accountMappingTypeId, chargeId); - BigDecimal amount = chargePaymentDTO.getAmount(); - - creditDetailsMap.merge(account, amount, BigDecimal::add); - } - - BigDecimal totalCreditedAmount = BigDecimal.ZERO; - - for (Map.Entry entry : creditDetailsMap.entrySet()) { - GLAccount account = entry.getKey(); - BigDecimal amount = entry.getValue(); - totalCreditedAmount = totalCreditedAmount.add(amount); - - if (isCredit) { - createCreditJournalEntryForLoan(office, currencyCode, account, loanId, transactionId, transactionDate, amount); - } else { - createDebitJournalEntryForLoan(office, currencyCode, account, loanId, transactionId, transactionDate, amount); - } - } - - if (totalAmount.compareTo(totalCreditedAmount) != 0) { - throw new PlatformDataIntegrityException( - "Meltdown in advanced accounting...sum of all charges is not equal to the fee charge for a transaction", - "Meltdown in advanced accounting...sum of all charges is not equal to the fee charge for a transaction", - totalCreditedAmount, totalAmount); - } - } -} +/** + * 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.accounting.journalentry.service; + +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 java.util.Objects; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.accounting.closure.domain.GLClosure; +import org.apache.fineract.accounting.closure.domain.GLClosureRepository; +import org.apache.fineract.accounting.common.AccountingConstants; +import org.apache.fineract.accounting.common.AccountingConstants.AccrualAccountsForLoan; +import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForLoan; +import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForSavings; +import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForShares; +import org.apache.fineract.accounting.common.AccountingConstants.FinancialActivity; +import org.apache.fineract.accounting.common.AccountingConstants.LoanProductAccountingParams; +import org.apache.fineract.accounting.financialactivityaccount.domain.FinancialActivityAccount; +import org.apache.fineract.accounting.financialactivityaccount.domain.FinancialActivityAccountRepositoryWrapper; +import org.apache.fineract.accounting.glaccount.domain.GLAccount; +import org.apache.fineract.accounting.glaccount.domain.GLAccountRepository; +import org.apache.fineract.accounting.journalentry.data.ChargePaymentDTO; +import org.apache.fineract.accounting.journalentry.data.ClientChargePaymentDTO; +import org.apache.fineract.accounting.journalentry.data.ClientTransactionDTO; +import org.apache.fineract.accounting.journalentry.data.LoanDTO; +import org.apache.fineract.accounting.journalentry.data.LoanTransactionDTO; +import org.apache.fineract.accounting.journalentry.data.SavingsDTO; +import org.apache.fineract.accounting.journalentry.data.SavingsTransactionDTO; +import org.apache.fineract.accounting.journalentry.data.SharesDTO; +import org.apache.fineract.accounting.journalentry.data.SharesTransactionDTO; +import org.apache.fineract.accounting.journalentry.data.TaxPaymentDTO; +import org.apache.fineract.accounting.journalentry.domain.JournalEntry; +import org.apache.fineract.accounting.journalentry.domain.JournalEntryRepository; +import org.apache.fineract.accounting.journalentry.domain.JournalEntryType; +import org.apache.fineract.accounting.journalentry.exception.JournalEntryInvalidException; +import org.apache.fineract.accounting.journalentry.exception.JournalEntryInvalidException.GlJournalEntryInvalidReason; +import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMapping; +import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMappingRepository; +import org.apache.fineract.accounting.producttoaccountmapping.exception.ProductToGLAccountMappingNotFoundException; +import org.apache.fineract.infrastructure.core.data.EnumOptionData; +import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.MathUtil; +import org.apache.fineract.infrastructure.event.business.domain.journalentry.LoanJournalEntryCreatedBusinessEvent; +import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; +import org.apache.fineract.organisation.office.domain.Office; +import org.apache.fineract.organisation.office.domain.OfficeRepository; +import org.apache.fineract.portfolio.PortfolioProductType; +import org.apache.fineract.portfolio.account.PortfolioAccountType; +import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService; +import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper; +import org.apache.fineract.portfolio.loanaccount.data.AccountingBridgeLoanTransactionDTO; +import org.apache.fineract.portfolio.loanaccount.data.LoanChargeData; +import org.apache.fineract.portfolio.loanaccount.data.LoanChargePaidByDTO; +import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionEnumData; +import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionEnumData; +import org.apache.fineract.portfolio.shareaccounts.data.ShareAccountTransactionEnumData; +import org.springframework.dao.DataAccessException; + +@RequiredArgsConstructor +public class AccountingProcessorHelper { + + public static final String LOAN_TRANSACTION_IDENTIFIER = "L"; + public static final String SAVINGS_TRANSACTION_IDENTIFIER = "S"; + public static final String FIXED_DEPOSIT_TRANSACTION_IDENTIFIER = "FD"; + public static final String RECURRING_DEPOSIT_TRANSACTION_IDENTIFIER = "RD"; + public static final String CLIENT_TRANSACTION_IDENTIFIER = "C"; + public static final String PROVISIONING_TRANSACTION_IDENTIFIER = "P"; + public static final String SHARE_TRANSACTION_IDENTIFIER = "SH"; + + private static final ThreadLocal DEPOSIT_ACCOUNT_TYPE_CONTEXT = ThreadLocal.withInitial(() -> 100); + + public static void setDepositAccountTypeContext(int depositAccountType) { + DEPOSIT_ACCOUNT_TYPE_CONTEXT.set(depositAccountType); + } + + public static void clearDepositAccountTypeContext() { + DEPOSIT_ACCOUNT_TYPE_CONTEXT.remove(); + } + + private String getSavingsTransactionIdentifier() { + return switch (DEPOSIT_ACCOUNT_TYPE_CONTEXT.get()) { + case 200 -> FIXED_DEPOSIT_TRANSACTION_IDENTIFIER; + case 300 -> RECURRING_DEPOSIT_TRANSACTION_IDENTIFIER; + default -> SAVINGS_TRANSACTION_IDENTIFIER; + }; + } + + private final JournalEntryRepository glJournalEntryRepository; + private final ProductToGLAccountMappingRepository accountMappingRepository; + private final FinancialActivityAccountRepositoryWrapper financialActivityAccountRepository; + private final GLClosureRepository closureRepository; + private final GLAccountRepository glAccountRepository; + private final OfficeRepository officeRepository; + private final AccountTransfersReadPlatformService accountTransfersReadPlatformService; + private final ChargeRepositoryWrapper chargeRepositoryWrapper; + private final BusinessEventNotifierService businessEventNotifierService; + + public LoanDTO populateLoanDtoFromDTO( + final org.apache.fineract.portfolio.loanaccount.data.AccountingBridgeDataDTO accountingBridgeData) { + final Long loanId = accountingBridgeData.getLoanId(); + final Long loanProductId = accountingBridgeData.getLoanProductId(); + final Long officeId = accountingBridgeData.getOfficeId(); + final String currencyCode = accountingBridgeData.getCurrencyCode(); + final List newLoanTransactions = new ArrayList<>(); + boolean isAccountTransfer = accountingBridgeData.isAccountTransfer(); + boolean isLoanMarkedAsChargeOff = accountingBridgeData.isChargeOff(); + boolean isLoanMarkedAsFraud = accountingBridgeData.isFraud(); + final Long chargeOffReasonCodeValue = accountingBridgeData.getChargeOffReasonCodeValue(); + final boolean isLoanMarkedAsWrittenOff = accountingBridgeData.isWrittenOff(); + final boolean cashBasedAccountingEnabled = accountingBridgeData.isCashBasedAccountingEnabled(); + final boolean upfrontAccrualBasedAccountingEnabled = accountingBridgeData.isUpfrontAccrualBasedAccountingEnabled(); + final boolean periodicAccrualBasedAccountingEnabled = accountingBridgeData.isPeriodicAccrualBasedAccountingEnabled(); + final boolean merchantBuyDownFee = accountingBridgeData.isMerchantBuyDownFee(); + + final List loanTransactionDTOs = accountingBridgeData.getNewLoanTransactions(); + + for (final AccountingBridgeLoanTransactionDTO loanTxnDto : loanTransactionDTOs) { + final Long transactionOfficeId = loanTxnDto.getOfficeId(); + final String transactionId = loanTxnDto.getId().toString(); + final LocalDate transactionDate = loanTxnDto.getDate(); + final LoanTransactionEnumData transactionType = loanTxnDto.getType(); + final BigDecimal amount = loanTxnDto.getAmount(); + final BigDecimal principal = loanTxnDto.getPrincipalPortion(); + final BigDecimal interest = loanTxnDto.getInterestPortion(); + final BigDecimal fees = loanTxnDto.getFeeChargesPortion(); + final BigDecimal penalties = loanTxnDto.getPenaltyChargesPortion(); + final BigDecimal overPayments = loanTxnDto.getOverPaymentPortion(); + final boolean reversed = loanTxnDto.isReversed(); + final Long paymentTypeId = loanTxnDto.getPaymentTypeId(); + final String chargeRefundChargeType = loanTxnDto.getChargeRefundChargeType(); + final LoanChargeData loanChargeData = loanTxnDto.getLoanChargeData(); + + final List feePaymentDetails = new ArrayList<>(); + final List penaltyPaymentDetails = new ArrayList<>(); + // extract charge payment details (if exists) + if (loanTxnDto.getLoanChargesPaid() != null) { + List loanChargesPaidData = loanTxnDto.getLoanChargesPaid(); + for (final LoanChargePaidByDTO loanChargePaid : loanChargesPaidData) { + final Long chargeId = loanChargePaid.getChargeId(); + final Long loanChargeId = loanChargePaid.getLoanChargeId(); + final boolean isPenalty = loanChargePaid.getIsPenalty(); + final BigDecimal chargeAmountPaid = loanChargePaid.getAmount(); + final ChargePaymentDTO chargePaymentDTO = new ChargePaymentDTO(chargeId, chargeAmountPaid, loanChargeId); + if (isPenalty) { + penaltyPaymentDetails.add(chargePaymentDTO); + } else { + feePaymentDetails.add(chargePaymentDTO); + } + } + } + + boolean localIsAccountTransfer = isAccountTransfer; + if (!localIsAccountTransfer) { + localIsAccountTransfer = this.accountTransfersReadPlatformService.isAccountTransfer(Long.parseLong(transactionId), + PortfolioAccountType.LOAN); + } + + BigDecimal principalPaid = loanTxnDto.getPrincipalPaid(); + BigDecimal feePaid = loanTxnDto.getFeePaid(); + BigDecimal penaltyPaid = loanTxnDto.getPenaltyPaid(); + + final LoanTransactionDTO transaction = new LoanTransactionDTO(transactionOfficeId, paymentTypeId, transactionId, + transactionDate, transactionType, amount, principal, interest, fees, penalties, overPayments, reversed, + penaltyPaymentDetails, feePaymentDetails, localIsAccountTransfer, chargeRefundChargeType, loanChargeData, principalPaid, + feePaid, penaltyPaid); + + transaction.setLoanToLoanTransfer(loanTxnDto.isLoanToLoanTransfer()); + newLoanTransactions.add(transaction); + } + + return new LoanDTO(loanId, loanProductId, officeId, currencyCode, cashBasedAccountingEnabled, upfrontAccrualBasedAccountingEnabled, + periodicAccrualBasedAccountingEnabled, newLoanTransactions, isLoanMarkedAsChargeOff, isLoanMarkedAsFraud, + chargeOffReasonCodeValue, isLoanMarkedAsWrittenOff, merchantBuyDownFee, + accountingBridgeData.getBuydownFeeClassificationCodeValue(), + accountingBridgeData.getCapitalizedIncomeClassificationCodeValue(), accountingBridgeData.getWriteOffReasonCodeValue()); + } + + public ProductToGLAccountMapping getChargeOffMappingByCodeValue(Long loanProductId, PortfolioProductType productType, + Long chargeOffReasonId) { + return accountMappingRepository.findChargeOffReasonMapping(loanProductId, productType.getValue(), chargeOffReasonId); + } + + public ProductToGLAccountMapping getWriteOffMappingByCodeValue(Long loanProductId, PortfolioProductType productType, + Long writeOffReasonId) { + return accountMappingRepository.findWriteOffReasonMapping(loanProductId, productType.getValue(), writeOffReasonId); + } + + public ProductToGLAccountMapping getClassificationMappingByCodeValue(Long loanProductId, PortfolioProductType productType, + final Long classificationId, final String classificationType) { + if (LoanProductAccountingParams.BUYDOWN_FEE_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS.getValue().equals(classificationType)) { + return accountMappingRepository.findBuydownFeeClassificationMapping(loanProductId, productType.getValue(), classificationId); + } else { + return accountMappingRepository.findCapitalizedIncomeClassificationMapping(loanProductId, productType.getValue(), + classificationId); + } + } + + public SavingsDTO populateSavingsDtoFromMap(final Map accountingBridgeData, final boolean cashBasedAccountingEnabled, + final boolean accrualBasedAccountingEnabled) { + final Long loanId = (Long) accountingBridgeData.get("savingsId"); + final Long loanProductId = (Long) accountingBridgeData.get("savingsProductId"); + final Long officeId = (Long) accountingBridgeData.get("officeId"); + final String currencyCode = (String) accountingBridgeData.get("currencyCode"); + final int depositAccountType = accountingBridgeData.containsKey("depositAccountType") + ? (Integer) accountingBridgeData.get("depositAccountType") + : 100; + final List newSavingsTransactions = new ArrayList<>(); + boolean isAccountTransfer = (Boolean) accountingBridgeData.get("isAccountTransfer"); + + @SuppressWarnings("unchecked") + final List> newTransactionsMap = (List>) accountingBridgeData.get("newSavingsTransactions"); + + for (final Map map : newTransactionsMap) { + final Long transactionOfficeId = (Long) map.get("officeId"); + final String transactionId = ((Long) map.get("id")).toString(); + final LocalDate transactionDate = ((LocalDate) map.get("date")); + final SavingsAccountTransactionEnumData transactionType = (SavingsAccountTransactionEnumData) map.get("type"); + final BigDecimal amount = (BigDecimal) map.get("amount"); + final boolean reversed = (Boolean) map.get("reversed"); + final Long paymentTypeId = (Long) map.get("paymentTypeId"); + final BigDecimal overdraftAmount = (BigDecimal) map.get("overdraftAmount"); + + final List feePayments = new ArrayList<>(); + final List penaltyPayments = new ArrayList<>(); + // extract charge payment details (if exists) + if (map.containsKey("savingsChargesPaid")) { + @SuppressWarnings("unchecked") + final List> savingsChargesPaidData = (List>) map.get("savingsChargesPaid"); + for (final Map loanChargePaid : savingsChargesPaidData) { + final Long chargeId = (Long) loanChargePaid.get("chargeId"); + final Long loanChargeId = (Long) loanChargePaid.get("savingsChargeId"); + final boolean isPenalty = (Boolean) loanChargePaid.get("isPenalty"); + final BigDecimal chargeAmountPaid = (BigDecimal) loanChargePaid.get("amount"); + final ChargePaymentDTO chargePaymentDTO = new ChargePaymentDTO(chargeId, chargeAmountPaid, loanChargeId); + if (isPenalty) { + penaltyPayments.add(chargePaymentDTO); + } else { + feePayments.add(chargePaymentDTO); + } + } + } + + final List taxPayments = new ArrayList<>(); + if (map.containsKey("taxDetails")) { + @SuppressWarnings("unchecked") + final List> taxDataList = (List>) map.get("taxDetails"); + for (final Map taxData : taxDataList) { + final BigDecimal taxAmount = (BigDecimal) taxData.get("amount"); + final Long creditAccountId = (Long) taxData.get("creditAccountId"); + final Long debitAccountId = (Long) taxData.get("debitAccountId"); + taxPayments.add(new TaxPaymentDTO(debitAccountId, creditAccountId, taxAmount)); + } + } + + if (!isAccountTransfer) { + isAccountTransfer = this.accountTransfersReadPlatformService.isAccountTransfer(Long.parseLong(transactionId), + PortfolioAccountType.SAVINGS); + } + final SavingsTransactionDTO transaction = new SavingsTransactionDTO(transactionOfficeId, paymentTypeId, transactionId, + transactionDate, transactionType, amount, reversed, feePayments, penaltyPayments, overdraftAmount, isAccountTransfer, + taxPayments); + + newSavingsTransactions.add(transaction); + + } + + return new SavingsDTO(loanId, loanProductId, officeId, currencyCode, cashBasedAccountingEnabled, accrualBasedAccountingEnabled, + newSavingsTransactions, depositAccountType); + } + + public SharesDTO populateSharesDtoFromMap(final Map accountingBridgeData, final boolean cashBasedAccountingEnabled, + final boolean accrualBasedAccountingEnabled) { + final Long shareAccountId = (Long) accountingBridgeData.get("shareAccountId"); + final Long shareProductId = (Long) accountingBridgeData.get("shareProductId"); + final Long officeId = (Long) accountingBridgeData.get("officeId"); + final String currencyCode = (String) accountingBridgeData.get("currencyCode"); + final List newTransactions = new ArrayList<>(); + + @SuppressWarnings("unchecked") + final List> newTransactionsMap = (List>) accountingBridgeData.get("newTransactions"); + + for (final Map map : newTransactionsMap) { + final Long transactionOfficeId = (Long) map.get("officeId"); + final String transactionId = ((Long) map.get("id")).toString(); + final LocalDate transactionDate = ((LocalDate) map.get("date")); + final ShareAccountTransactionEnumData transactionType = (ShareAccountTransactionEnumData) map.get("type"); + final ShareAccountTransactionEnumData transactionStatus = (ShareAccountTransactionEnumData) map.get("status"); + final BigDecimal amount = (BigDecimal) map.get("amount"); + final BigDecimal chargeAmount = (BigDecimal) map.get("chargeAmount"); + final Long paymentTypeId = (Long) map.get("paymentTypeId"); + + final List feePayments = new ArrayList<>(); + // extract charge payment details (if exists) + if (map.containsKey("chargesPaid")) { + @SuppressWarnings("unchecked") + final List> chargesPaidData = (List>) map.get("chargesPaid"); + for (final Map chargePaid : chargesPaidData) { + final Long chargeId = (Long) chargePaid.get("chargeId"); + final Long loanChargeId = (Long) chargePaid.get("sharesChargeId"); + final BigDecimal chargeAmountPaid = (BigDecimal) chargePaid.get("amount"); + final ChargePaymentDTO chargePaymentDTO = new ChargePaymentDTO(chargeId, chargeAmountPaid, loanChargeId); + feePayments.add(chargePaymentDTO); + } + } + final SharesTransactionDTO transaction = new SharesTransactionDTO(transactionOfficeId, paymentTypeId, transactionId, + transactionDate, transactionType, transactionStatus, amount, chargeAmount, feePayments); + + newTransactions.add(transaction); + + } + + return new SharesDTO(shareAccountId, shareProductId, officeId, currencyCode, cashBasedAccountingEnabled, + accrualBasedAccountingEnabled, newTransactions); + } + + public ClientTransactionDTO populateClientTransactionDtoFromMap(final Map accountingBridgeData) { + + final Long transactionOfficeId = (Long) accountingBridgeData.get("officeId"); + final Long clientId = (Long) accountingBridgeData.get("clientId"); + final Long transactionId = (Long) accountingBridgeData.get("id"); + final LocalDate transactionDate = ((LocalDate) accountingBridgeData.get("date")); + final EnumOptionData transactionType = (EnumOptionData) accountingBridgeData.get("type"); + final BigDecimal amount = (BigDecimal) accountingBridgeData.get("amount"); + final boolean reversed = (Boolean) accountingBridgeData.get("reversed"); + final Long paymentTypeId = (Long) accountingBridgeData.get("paymentTypeId"); + final String currencyCode = (String) accountingBridgeData.get("currencyCode"); + final Boolean accountingEnabled = (Boolean) accountingBridgeData.get("accountingEnabled"); + + final List clientChargePaymentDTOs = new ArrayList<>(); + // extract client charge payment details (if exists) + if (accountingBridgeData.containsKey("clientChargesPaid")) { + @SuppressWarnings("unchecked") + final List> clientChargesPaidData = (List>) accountingBridgeData + .get("clientChargesPaid"); + for (final Map clientChargePaid : clientChargesPaidData) { + final Long chargeId = (Long) clientChargePaid.get("chargeId"); + final Long clientChargeId = (Long) clientChargePaid.get("clientChargeId"); + final boolean isPenalty = (Boolean) clientChargePaid.get("isPenalty"); + final BigDecimal chargeAmountPaid = (BigDecimal) clientChargePaid.get("amount"); + final Long incomeAccountId = (Long) clientChargePaid.get("incomeAccountId"); + final ClientChargePaymentDTO clientChargePaymentDTO = new ClientChargePaymentDTO(chargeId, chargeAmountPaid, clientChargeId, + isPenalty, incomeAccountId); + clientChargePaymentDTOs.add(clientChargePaymentDTO); + } + } + + return new ClientTransactionDTO(clientId, transactionOfficeId, paymentTypeId, transactionId, transactionDate, transactionType, + currencyCode, amount, reversed, accountingEnabled, clientChargePaymentDTOs); + + } + + /** + * Convenience method that creates a pair of related Debits and Credits for Accrual Based accounting. + * + * The target accounts for debits and credits are switched in case of a reversal + * + * @param office + * office + * @param currencyCode + * currencyCode + * @param accountTypeToBeDebited + * Enum of the placeholder GLAccount to be debited + * @param accountTypeToBeCredited + * Enum of the placeholder of the GLAccount to be credited + * @param loanProductId + * loanProductId + * @param loanId + * loanId + * @param transactionId + * transactionId + * @param transactionDate + * transactionDate + * @param totalAmount + * totalAmount + * @param chargePaymentDTOs + * chargePaymentDTOs + */ + public void createJournalEntriesForLoanCharges(final Office office, final String currencyCode, final Integer accountTypeToBeDebited, + final Integer accountTypeToBeCredited, final Long loanProductId, final Long loanId, final String transactionId, + final LocalDate transactionDate, final BigDecimal totalAmount, final List chargePaymentDTOs) { + + final Map creditDetailsMap = new LinkedHashMap<>(); + final Map debitDetailsMap = new LinkedHashMap<>(); + + for (final ChargePaymentDTO chargePaymentDTO : chargePaymentDTOs) { + final Long chargeId = chargePaymentDTO.getChargeId(); + final GLAccount chargeSpecificCreditAccount = getLinkedGLAccountForLoanCharges(loanProductId, accountTypeToBeCredited, + chargeId); + final GLAccount chargeSpecificDebitAccount = getLinkedGLAccountForLoanCharges(loanProductId, accountTypeToBeDebited, chargeId); + final BigDecimal chargeSpecificAmount = chargePaymentDTO.getAmount(); + + // aggregate amounts by account for credit entries + creditDetailsMap.merge(chargeSpecificCreditAccount, chargeSpecificAmount, BigDecimal::add); + + // aggregate amounts by account for debit entries + debitDetailsMap.merge(chargeSpecificDebitAccount, chargeSpecificAmount, BigDecimal::add); + } + + BigDecimal totalCreditedAmount = BigDecimal.ZERO; + BigDecimal totalDebitedAmount = BigDecimal.ZERO; + + // Create credit journal entries + for (final Map.Entry entry : creditDetailsMap.entrySet()) { + final GLAccount account = entry.getKey(); + final BigDecimal amount = entry.getValue(); + totalCreditedAmount = totalCreditedAmount.add(amount); + createCreditJournalEntryForLoan(office, currencyCode, account, loanId, transactionId, transactionDate, amount); + } + + // Create debit journal entries using charge-specific debit accounts + for (final Map.Entry entry : debitDetailsMap.entrySet()) { + final GLAccount account = entry.getKey(); + final BigDecimal amount = entry.getValue(); + totalDebitedAmount = totalDebitedAmount.add(amount); + createDebitJournalEntryForLoan(office, currencyCode, account, loanId, transactionId, transactionDate, amount); + } + + if (totalAmount.compareTo(totalCreditedAmount) != 0) { + throw new PlatformDataIntegrityException( + "Meltdown in advanced accounting...sum of all charge credits does not equal the total transaction amount", + "Sum of charge credits (" + totalCreditedAmount + ") does not equal transaction total (" + totalAmount + ") for loan " + + loanId + ", transaction " + transactionId, + totalCreditedAmount, totalAmount); + } + + if (totalAmount.compareTo(totalDebitedAmount) != 0) { + throw new PlatformDataIntegrityException( + "Meltdown in advanced accounting...sum of all charge debits does not equal the total transaction amount", + "Sum of charge debits (" + totalDebitedAmount + ") does not equal transaction total (" + totalAmount + ") for loan " + + loanId + ", transaction " + transactionId, + totalDebitedAmount, totalAmount); + } + } + + /** + * Convenience method that creates a pair of related Debits and Credits for Cash Based accounting. + * + * The target accounts for debits and credits are switched in case of a reversal + * + * @param office + * @param accountTypeToBeDebited + * Enum of the placeholder GLAccount to be debited + * @param accountTypeToBeCredited + * Enum of the placeholder of the GLAccount to be credited + * @param savingsProductId + * @param paymentTypeId + * @param loanId + * @param transactionId + * @param transactionDate + * @param amount + * @param isReversal + */ + public void createCashBasedJournalEntriesAndReversalsForSavings(final Office office, final String currencyCode, + final Integer accountTypeToBeDebited, final Integer accountTypeToBeCredited, final Long savingsProductId, + final Long paymentTypeId, final Long loanId, final String transactionId, final LocalDate transactionDate, + final BigDecimal amount, final Boolean isReversal) { + int accountTypeToDebitId = accountTypeToBeDebited; + int accountTypeToCreditId = accountTypeToBeCredited; + // reverse debits and credits for reversals + if (isReversal) { + accountTypeToDebitId = accountTypeToBeCredited; + accountTypeToCreditId = accountTypeToBeDebited; + } + createJournalEntriesForSavings(office, currencyCode, accountTypeToDebitId, accountTypeToCreditId, savingsProductId, paymentTypeId, + loanId, transactionId, transactionDate, amount); + } + + /** + * Convenience method that creates a pair of related Debits and Credits for Cash Based accounting. + * + * The target accounts for debits and credits are switched in case of a reversal + * + * @param office + * @param accountTypeToBeDebited + * Enum of the placeholder GLAccount to be debited + * @param accountTypeToBeCredited + * Enum of the placeholder of the GLAccount to be credited + * @param loanProductId + * @param paymentTypeId + * @param loanId + * @param transactionId + * @param transactionDate + * @param amount + */ + public void createJournalEntriesForLoan(final Office office, final String currencyCode, final Integer accountTypeToBeDebited, + final Integer accountTypeToBeCredited, final Long loanProductId, final Long paymentTypeId, final Long loanId, + final String transactionId, final LocalDate transactionDate, final BigDecimal amount) { + int accountTypeToDebitId = accountTypeToBeDebited; + int accountTypeToCreditId = accountTypeToBeCredited; + createJournalEntriesForLoan(office, currencyCode, accountTypeToDebitId, accountTypeToCreditId, loanProductId, paymentTypeId, loanId, + transactionId, transactionDate, amount); + } + + public void createJournalEntriesForLoan(final Office office, final String currencyCode, final Integer accountTypeToBeDebited, + final GLAccount accountToBeCredited, final Long loanProductId, final Long paymentTypeId, final Long loanId, + final String transactionId, final LocalDate transactionDate, final BigDecimal amount) { + int accountTypeToDebitId = accountTypeToBeDebited; + createJournalEntriesForLoan(office, currencyCode, accountTypeToDebitId, accountToBeCredited, loanProductId, paymentTypeId, loanId, + transactionId, transactionDate, amount); + } + + public void createSplitJournalEntriesForLoan(Office office, String currencyCode, List splitAccountsHolder, + JournalAmountHolder totalAccountHolder, Long loanProductId, Long paymentTypeId, Long loanId, String transactionId, + LocalDate transactionDate) { + splitAccountsHolder.forEach(journalItemHolder -> { + if (MathUtil.isGreaterThanZero(journalItemHolder.getAmount())) { + final GLAccount account = getLinkedGLAccountForLoanProduct(loanProductId, journalItemHolder.getAccountType(), + paymentTypeId); + createDebitJournalEntryForLoan(office, currencyCode, account, loanId, transactionId, transactionDate, + journalItemHolder.getAmount()); + } + }); + if (MathUtil.isGreaterThanZero(totalAccountHolder.getAmount())) { + final GLAccount totalAccount = getLinkedGLAccountForLoanProduct(loanProductId, totalAccountHolder.getAccountType(), + paymentTypeId); + createCreditJournalEntryForLoan(office, currencyCode, totalAccount, loanId, transactionId, transactionDate, + totalAccountHolder.getAmount()); + } + } + + public void createCreditJournalEntryForLoan(final Office office, final String currencyCode, + final CashAccountsForLoan accountMappingType, final Long loanProductId, final Long paymentTypeId, final Long loanId, + final String transactionId, final LocalDate transactionDate, final BigDecimal amount) { + final int accountMappingTypeId = accountMappingType.getValue(); + createCreditJournalEntryForLoan(office, currencyCode, accountMappingTypeId, loanProductId, paymentTypeId, loanId, transactionId, + transactionDate, amount); + } + + public void createCreditJournalEntryForLoan(final Office office, final String currencyCode, + final AccrualAccountsForLoan accountMappingType, final Long loanProductId, final Long paymentTypeId, final Long loanId, + final String transactionId, final LocalDate transactionDate, final BigDecimal amount) { + final int accountMappingTypeId = accountMappingType.getValue(); + createCreditJournalEntryForLoan(office, currencyCode, accountMappingTypeId, loanProductId, paymentTypeId, loanId, transactionId, + transactionDate, amount); + } + + /** + * @param latestGLClosure + * @param transactionDate + */ + public void checkForBranchClosures(final GLClosure latestGLClosure, final LocalDate transactionDate) { + // check if an accounting closure has happened for this branch after the transaction Date + if (latestGLClosure != null) { + if (!DateUtils.isBefore(latestGLClosure.getClosingDate(), transactionDate)) { + throw new JournalEntryInvalidException(GlJournalEntryInvalidReason.ACCOUNTING_CLOSED, latestGLClosure.getClosingDate(), + null, null); + } + } + } + + public GLClosure getLatestClosureByBranch(final long officeId) { + return this.closureRepository.getLatestGLClosureByBranch(officeId); + } + + private void createJournalEntriesForLoan(final Office office, final String currencyCode, final int accountTypeToDebitId, + final int accountTypeToCreditId, final Long loanProductId, final Long paymentTypeId, final Long loanId, + final String transactionId, final LocalDate transactionDate, final BigDecimal amount) { + final GLAccount debitAccount = getLinkedGLAccountForLoanProduct(loanProductId, accountTypeToDebitId, paymentTypeId); + final GLAccount creditAccount = getLinkedGLAccountForLoanProduct(loanProductId, accountTypeToCreditId, paymentTypeId); + createDebitJournalEntryForLoan(office, currencyCode, debitAccount, loanId, transactionId, transactionDate, amount); + createCreditJournalEntryForLoan(office, currencyCode, creditAccount, loanId, transactionId, transactionDate, amount); + } + + private void createJournalEntriesForLoan(final Office office, final String currencyCode, final int accountTypeToDebitId, + final GLAccount creditAccount, final Long loanProductId, final Long paymentTypeId, final Long loanId, + final String transactionId, final LocalDate transactionDate, final BigDecimal amount) { + final GLAccount debitAccount = getLinkedGLAccountForLoanProduct(loanProductId, accountTypeToDebitId, paymentTypeId); + createDebitJournalEntryForLoan(office, currencyCode, debitAccount, loanId, transactionId, transactionDate, amount); + createCreditJournalEntryForLoan(office, currencyCode, creditAccount, loanId, transactionId, transactionDate, amount); + } + + private void createJournalEntriesForSavings(final Office office, final String currencyCode, final int accountTypeToDebitId, + final int accountTypeToCreditId, final Long savingsProductId, final Long paymentTypeId, final Long savingsId, + final String transactionId, final LocalDate transactionDate, final BigDecimal amount) { + final GLAccount debitAccount = getLinkedGLAccountForSavingsProduct(savingsProductId, accountTypeToDebitId, paymentTypeId); + final GLAccount creditAccount = getLinkedGLAccountForSavingsProduct(savingsProductId, accountTypeToCreditId, paymentTypeId); + createDebitJournalEntryForSavings(office, currencyCode, debitAccount, savingsId, transactionId, transactionDate, amount); + createCreditJournalEntryForSavings(office, currencyCode, creditAccount, savingsId, transactionId, transactionDate, amount); + } + + /** + * Convenience method that creates a pair of related Debits and Credits for Cash Based accounting. + * + * The target accounts for debits and credits are switched in case of a reversal + * + * @param office + * @param currencyCode + * @param accountTypeToBeDebited + * Enum of the placeholder GLAccount to be debited + * @param accountTypeToBeCredited + * Enum of the placeholder of the GLAccount to be credited + * @param savingsProductId + * @param paymentTypeId + * @param savingsId + * @param transactionId + * @param transactionDate + * @param amount + * @param isReversal + * @param taxDetails + */ + public void createCashBasedJournalEntriesAndReversalsForSavingsTax(final Office office, final String currencyCode, + final CashAccountsForSavings accountTypeToBeDebited, final CashAccountsForSavings accountTypeToBeCredited, + final Long savingsProductId, final Long paymentTypeId, final Long savingsId, final String transactionId, + final LocalDate transactionDate, final BigDecimal amount, final Boolean isReversal, final List taxDetails) { + + for (TaxPaymentDTO taxPaymentDTO : taxDetails) { + if (taxPaymentDTO.getAmount() != null) { + if (taxPaymentDTO.getCreditAccountId() == null) { + createCashBasedCreditJournalEntriesAndReversalsForSavings(office, currencyCode, accountTypeToBeCredited.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, taxPaymentDTO.getAmount(), + isReversal); + } else { + createCashBasedCreditJournalEntriesAndReversalsForSavings(office, currencyCode, taxPaymentDTO.getCreditAccountId(), + savingsId, transactionId, transactionDate, taxPaymentDTO.getAmount(), isReversal); + } + } + } + createCashBasedDebitJournalEntriesAndReversalsForSavings(office, currencyCode, accountTypeToBeDebited.getValue(), savingsProductId, + paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); + } + + public void createAccrualBasedJournalEntriesAndReversalsForSavingsTax(final Office office, final String currencyCode, + final AccountingConstants.AccrualAccountsForSavings accountTypeToBeDebited, + final AccountingConstants.AccrualAccountsForSavings accountTypeToBeCredited, final Long savingsProductId, + final Long paymentTypeId, final Long savingsId, final String transactionId, final LocalDate transactionDate, + final BigDecimal amount, final Boolean isReversal, final List taxDetails) { + for (TaxPaymentDTO taxPaymentDTO : taxDetails) { + if (taxPaymentDTO.getAmount() != null) { + if (taxPaymentDTO.getCreditAccountId() == null) { + createAccrualBasedCreditJournalEntriesAndReversalsForSavings(office, currencyCode, accountTypeToBeCredited.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, taxPaymentDTO.getAmount(), + isReversal); + } else { + createAccrualBasedBasedCreditJournalEntriesAndReversalsForSavings(office, currencyCode, + taxPaymentDTO.getCreditAccountId(), savingsId, transactionId, transactionDate, taxPaymentDTO.getAmount(), + isReversal); + } + } + } + createAccrualBasedDebitJournalEntriesAndReversalsForSavings(office, currencyCode, accountTypeToBeDebited.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); + } + + public void createCashBasedDebitJournalEntriesAndReversalsForSavings(final Office office, final String currencyCode, + final Integer accountTypeToBeDebited, final Long savingsProductId, final Long paymentTypeId, final Long savingsId, + final String transactionId, final LocalDate transactionDate, final BigDecimal amount, final Boolean isReversal) { + // reverse debits and credits for reversals + if (isReversal) { + createCreditJournalEntriesForSavings(office, currencyCode, accountTypeToBeDebited, savingsProductId, paymentTypeId, savingsId, + transactionId, transactionDate, amount); + } else { + createDebitJournalEntriesForSavings(office, currencyCode, accountTypeToBeDebited, savingsProductId, paymentTypeId, savingsId, + transactionId, transactionDate, amount); + } + } + + public void createCashBasedCreditJournalEntriesAndReversalsForSavings(final Office office, final String currencyCode, + final Integer accountTypeToBeCredited, final Long savingsProductId, final Long paymentTypeId, final Long savingsId, + final String transactionId, final LocalDate transactionDate, final BigDecimal amount, final Boolean isReversal) { + // reverse debits and credits for reversals + if (isReversal) { + createDebitJournalEntriesForSavings(office, currencyCode, accountTypeToBeCredited, savingsProductId, paymentTypeId, savingsId, + transactionId, transactionDate, amount); + } else { + createCreditJournalEntriesForSavings(office, currencyCode, accountTypeToBeCredited, savingsProductId, paymentTypeId, savingsId, + transactionId, transactionDate, amount); + } + } + + public void createCashBasedCreditJournalEntriesAndReversalsForSavings(final Office office, final String currencyCode, + final Long creditAccountId, final Long savingsId, final String transactionId, final LocalDate transactionDate, + final BigDecimal amount, final Boolean isReversal) { + // reverse debits and credits for reversals + final GLAccount creditAccount = getGLAccountById(creditAccountId); + if (isReversal) { + createDebitJournalEntryForSavings(office, currencyCode, creditAccount, savingsId, transactionId, transactionDate, amount); + } else { + createCreditJournalEntryForSavings(office, currencyCode, creditAccount, savingsId, transactionId, transactionDate, amount); + } + } + + public void createAccrualBasedDebitJournalEntriesAndReversalsForSavings(final Office office, final String currencyCode, + final Integer accountTypeToBeDebited, final Long savingsProductId, final Long paymentTypeId, final Long savingsId, + final String transactionId, final LocalDate transactionDate, final BigDecimal amount, final Boolean isReversal) { + // reverse debits and credits for reversals + if (isReversal) { + createCreditJournalEntriesForSavings(office, currencyCode, accountTypeToBeDebited, savingsProductId, paymentTypeId, savingsId, + transactionId, transactionDate, amount); + } else { + createDebitJournalEntriesForSavings(office, currencyCode, accountTypeToBeDebited, savingsProductId, paymentTypeId, savingsId, + transactionId, transactionDate, amount); + } + } + + public void createAccrualBasedCreditJournalEntriesAndReversalsForSavings(final Office office, final String currencyCode, + final Integer accountTypeToBeCredited, final Long savingsProductId, final Long paymentTypeId, final Long savingsId, + final String transactionId, final LocalDate transactionDate, final BigDecimal amount, final Boolean isReversal) { + // reverse debits and credits for reversals + if (isReversal) { + createDebitJournalEntriesForSavings(office, currencyCode, accountTypeToBeCredited, savingsProductId, paymentTypeId, savingsId, + transactionId, transactionDate, amount); + } else { + createCreditJournalEntriesForSavings(office, currencyCode, accountTypeToBeCredited, savingsProductId, paymentTypeId, savingsId, + transactionId, transactionDate, amount); + } + } + + public void createAccrualBasedBasedCreditJournalEntriesAndReversalsForSavings(final Office office, final String currencyCode, + final Long creditAccountId, final Long savingsId, final String transactionId, final LocalDate transactionDate, + final BigDecimal amount, final Boolean isReversal) { + // reverse debits and credits for reversals + final GLAccount creditAccount = getGLAccountById(creditAccountId); + if (isReversal) { + createDebitJournalEntryForSavings(office, currencyCode, creditAccount, savingsId, transactionId, transactionDate, amount); + } else { + createCreditJournalEntryForSavings(office, currencyCode, creditAccount, savingsId, transactionId, transactionDate, amount); + } + } + + private void createDebitJournalEntriesForSavings(final Office office, final String currencyCode, final int accountTypeToDebitId, + final Long savingsProductId, final Long paymentTypeId, final Long savingsId, final String transactionId, + final LocalDate transactionDate, final BigDecimal amount) { + final GLAccount debitAccount = getLinkedGLAccountForSavingsProduct(savingsProductId, accountTypeToDebitId, paymentTypeId); + createDebitJournalEntryForSavings(office, currencyCode, debitAccount, savingsId, transactionId, transactionDate, amount); + } + + private void createCreditJournalEntriesForSavings(final Office office, final String currencyCode, final int accountTypeToCreditId, + final Long savingsProductId, final Long paymentTypeId, final Long savingsId, final String transactionId, + final LocalDate transactionDate, final BigDecimal amount) { + final GLAccount creditAccount = getLinkedGLAccountForSavingsProduct(savingsProductId, accountTypeToCreditId, paymentTypeId); + createCreditJournalEntryForSavings(office, currencyCode, creditAccount, savingsId, transactionId, transactionDate, amount); + } + + public void createDebitJournalEntryForLoan(final Office office, final String currencyCode, final int accountMappingTypeId, + final Long loanProductId, final Long paymentTypeId, final Long loanId, final String transactionId, + final LocalDate transactionDate, final BigDecimal amount) { + final GLAccount account = getLinkedGLAccountForLoanProduct(loanProductId, accountMappingTypeId, paymentTypeId); + createDebitJournalEntryForLoan(office, currencyCode, account, loanId, transactionId, transactionDate, amount); + } + + public void createDebitJournalEntryForLoan(final Office office, final String currencyCode, final Long loanId, + final String transactionId, final LocalDate transactionDate, final BigDecimal amount, final GLAccount account) { + createDebitJournalEntryForLoan(office, currencyCode, account, loanId, transactionId, transactionDate, amount); + } + + public void createDebitJournalEntryForLoanCharges(final Office office, final String currencyCode, final int accountMappingTypeId, + final Long loanProductId, final Long chargeId, final Long loanId, final String transactionId, final LocalDate transactionDate, + final BigDecimal amount) { + final GLAccount account = getLinkedGLAccountForLoanCharges(loanProductId, accountMappingTypeId, chargeId); + createDebitJournalEntryForLoan(office, currencyCode, account, loanId, transactionId, transactionDate, amount); + } + + public void createCreditJournalEntryForLoanCharges(final Office office, final String currencyCode, final int accountMappingTypeId, + final Long loanProductId, final Long loanId, final String transactionId, final LocalDate transactionDate, + final BigDecimal totalAmount, final List chargePaymentDTOs) { + createJournalEntriesForLoanChargesInternal(office, currencyCode, accountMappingTypeId, loanProductId, loanId, transactionId, + transactionDate, totalAmount, chargePaymentDTOs, true); + } + + public void createDebitJournalEntryForLoanCharges(final Office office, final String currencyCode, final int accountMappingTypeId, + final Long loanProductId, final Long loanId, final String transactionId, final LocalDate transactionDate, + final BigDecimal totalAmount, final List chargePaymentDTOs) { + createJournalEntriesForLoanChargesInternal(office, currencyCode, accountMappingTypeId, loanProductId, loanId, transactionId, + transactionDate, totalAmount, chargePaymentDTOs, false); + } + + /** + * Convenience method that creates a pair of related Debits and Credits for Cash Based accounting. + * + * The target accounts for debits and credits are switched in case of a reversal + * + * @param office + * office + * @param currencyCode + * currencyCode + * @param accountTypeToBeDebited + * Enum of the placeholder GLAccount to be debited + * @param accountTypeToBeCredited + * Enum of the placeholder of the GLAccount to be credited + * @param savingsProductId + * savingsProductId + * @param paymentTypeId + * paymentTypeId + * @param loanId + * loanId + * @param transactionId + * transactionId + * @param transactionDate + * transactionDate + * @param totalAmount + * totalAmount + * @param isReversal + * isReversal + * @param chargePaymentDTOs + * chargePaymentDTOs + */ + public void createCashBasedJournalEntriesAndReversalsForSavingsCharges(final Office office, final String currencyCode, + final CashAccountsForSavings accountTypeToBeDebited, CashAccountsForSavings accountTypeToBeCredited, + final Long savingsProductId, final Long paymentTypeId, final Long loanId, final String transactionId, + final LocalDate transactionDate, final BigDecimal totalAmount, final Boolean isReversal, + final List chargePaymentDTOs) { + // TODO Vishwas: Remove this validation, as and when appropriate Junit + // tests are written for accounting + /** + * Accounting module currently supports a single charge per transaction, throw an error if this is not the case + * here so any developers changing the expected portfolio behavior would also take care of modifying the + * accounting code appropriately + **/ + if (chargePaymentDTOs.size() != 1) { + throw new PlatformDataIntegrityException("Recent Portfolio changes w.r.t Charges for Savings have Broken the accounting code", + "Recent Portfolio changes w.r.t Charges for Savings have Broken the accounting code"); + } + ChargePaymentDTO chargePaymentDTO = chargePaymentDTOs.get(0); + GLAccount chargeSpecificAccount = getLinkedGLAccountForSavingsCharges(savingsProductId, accountTypeToBeCredited.getValue(), + chargePaymentDTO.getChargeId()); + + final GLAccount savingsControlAccount = getLinkedGLAccountForSavingsProduct(savingsProductId, accountTypeToBeDebited.getValue(), + paymentTypeId); + if (isReversal) { + createDebitJournalEntryForSavings(office, currencyCode, chargeSpecificAccount, loanId, transactionId, transactionDate, + totalAmount); + createCreditJournalEntryForSavings(office, currencyCode, savingsControlAccount, loanId, transactionId, transactionDate, + totalAmount); + } else { + createDebitJournalEntryForSavings(office, currencyCode, savingsControlAccount, loanId, transactionId, transactionDate, + totalAmount); + createCreditJournalEntryForSavings(office, currencyCode, chargeSpecificAccount, loanId, transactionId, transactionDate, + totalAmount); + } + } + + public void createAccrualBasedJournalEntriesAndReversalsForSavingsCharges(final Office office, final String currencyCode, + final AccountingConstants.AccrualAccountsForSavings accountTypeToBeDebited, + final AccountingConstants.AccrualAccountsForSavings accountTypeToBeCredited, final Long savingsProductId, + final Long paymentTypeId, final Long loanId, final String transactionId, final LocalDate transactionDate, + final BigDecimal totalAmount, final Boolean isReversal, final List chargePaymentDTOs) { + // TODO Vishwas: Remove this validation, as and when appropriate Junit + // tests are written for accounting + /** + * Accounting module currently supports a single charge per transaction, throw an error if this is not the case + * here so any developers changing the expected portfolio behavior would also take care of modifying the + * accounting code appropriately + **/ + if (chargePaymentDTOs.size() != 1) { + throw new PlatformDataIntegrityException("Recent Portfolio changes w.r.t Charges for Savings have Broken the accounting code", + "Recent Portfolio changes w.r.t Charges for Savings have Broken the accounting code"); + } + ChargePaymentDTO chargePaymentDTO = chargePaymentDTOs.get(0); + GLAccount chargeSpecificAccount = getLinkedGLAccountForSavingsCharges(savingsProductId, accountTypeToBeCredited.getValue(), + chargePaymentDTO.getChargeId()); + + final GLAccount savingsControlAccount = getLinkedGLAccountForSavingsProduct(savingsProductId, accountTypeToBeDebited.getValue(), + paymentTypeId); + if (isReversal) { + createDebitJournalEntryForSavings(office, currencyCode, chargeSpecificAccount, loanId, transactionId, transactionDate, + totalAmount); + createCreditJournalEntryForSavings(office, currencyCode, savingsControlAccount, loanId, transactionId, transactionDate, + totalAmount); + } else { + createDebitJournalEntryForSavings(office, currencyCode, savingsControlAccount, loanId, transactionId, transactionDate, + totalAmount); + createCreditJournalEntryForSavings(office, currencyCode, chargeSpecificAccount, loanId, transactionId, transactionDate, + totalAmount); + } + } + + public Office getOfficeById(final long officeId) { + return this.officeRepository.getReferenceById(officeId); + } + + public void createCreditJournalEntryForLoan(final Office office, final String currencyCode, final int accountMappingTypeId, + final Long loanProductId, final Long paymentTypeId, final Long loanId, final String transactionId, + final LocalDate transactionDate, final BigDecimal amount) { + final GLAccount account = getLinkedGLAccountForLoanProduct(loanProductId, accountMappingTypeId, paymentTypeId); + createCreditJournalEntryForLoan(office, currencyCode, loanId, transactionId, transactionDate, amount, account); + } + + public void createCreditJournalEntryForLoan(final Office office, final String currencyCode, final Long loanId, + final String transactionId, final LocalDate transactionDate, final BigDecimal amount, final GLAccount account) { + createCreditJournalEntryForLoan(office, currencyCode, account, loanId, transactionId, transactionDate, amount); + } + + private void createCreditJournalEntryForClientPayments(final Office office, final String currencyCode, final GLAccount account, + final Long clientId, final Long transactionId, final LocalDate transactionDate, final BigDecimal amount) { + final boolean manualEntry = false; + + String modifiedTransactionId = CLIENT_TRANSACTION_IDENTIFIER + transactionId; + final JournalEntry journalEntry = JournalEntry.createNew(office, null, account, currencyCode, modifiedTransactionId, manualEntry, + transactionDate, JournalEntryType.CREDIT, amount, null, PortfolioProductType.CLIENT.getValue(), clientId, null, null, null, + transactionId, null); + persistJournalEntry(journalEntry); + } + + private void createCreditJournalEntryForSavings(final Office office, final String currencyCode, final GLAccount account, + final Long savingsId, final String transactionId, final LocalDate transactionDate, final BigDecimal amount) + throws DataAccessException { + final boolean manualEntry = false; + Long savingsAccountTransactionId = null; + String modifiedTransactionId = transactionId; + if (StringUtils.isNumeric(transactionId)) { + savingsAccountTransactionId = Long.parseLong(transactionId); + modifiedTransactionId = getSavingsTransactionIdentifier() + transactionId; + } + final JournalEntry journalEntry = JournalEntry.createNew(office, null, account, currencyCode, modifiedTransactionId, manualEntry, + transactionDate, JournalEntryType.CREDIT, amount, null, PortfolioProductType.SAVING.getValue(), savingsId, null, null, + savingsAccountTransactionId, null, null); + + persistJournalEntry(journalEntry); + } + + private void createCreditJournalEntryForLoan(final Office office, final String currencyCode, final GLAccount account, final Long loanId, + final String transactionId, final LocalDate transactionDate, final BigDecimal amount) { + final boolean manualEntry = false; + Long loanTransactionId = null; + String modifiedTransactionId = transactionId; + if (StringUtils.isNumeric(transactionId)) { + loanTransactionId = Long.parseLong(transactionId); + modifiedTransactionId = LOAN_TRANSACTION_IDENTIFIER + transactionId; + } + final JournalEntry journalEntry = JournalEntry.createNew(office, null, account, currencyCode, modifiedTransactionId, manualEntry, + transactionDate, JournalEntryType.CREDIT, amount, null, PortfolioProductType.LOAN.getValue(), loanId, null, + loanTransactionId, null, null, null); + persistJournalEntry(journalEntry); + } + + public void createProvisioningDebitJournalEntry(LocalDate transactionDate, Long provisioningEntryId, Office office, String currencyCode, + GLAccount account, BigDecimal amount) { + final boolean manualEntry = false; + String modifiedTransactionId = PROVISIONING_TRANSACTION_IDENTIFIER + provisioningEntryId; + final JournalEntry journalEntry = JournalEntry.createNew(office, null, account, currencyCode, modifiedTransactionId, manualEntry, + transactionDate, JournalEntryType.DEBIT, amount, null, PortfolioProductType.PROVISIONING.getValue(), provisioningEntryId, + null, null, null, null, null); + persistJournalEntry(journalEntry); + } + + public void createProvisioningCreditJournalEntry(LocalDate transactionDate, Long provisioningEntryId, Office office, + String currencyCode, GLAccount account, BigDecimal amount) { + final boolean manualEntry = false; + String modifiedTransactionId = PROVISIONING_TRANSACTION_IDENTIFIER + provisioningEntryId; + final JournalEntry journalEntry = JournalEntry.createNew(office, null, account, currencyCode, modifiedTransactionId, manualEntry, + transactionDate, JournalEntryType.CREDIT, amount, null, PortfolioProductType.PROVISIONING.getValue(), provisioningEntryId, + null, null, null, null, null); + persistJournalEntry(journalEntry); + } + + public void createDebitJournalEntryForLoan(final Office office, final String currencyCode, final GLAccount account, final Long loanId, + final String transactionId, final LocalDate transactionDate, final BigDecimal amount) { + final boolean manualEntry = false; + Long loanTransactionId = null; + String modifiedTransactionId = transactionId; + if (StringUtils.isNumeric(transactionId)) { + loanTransactionId = Long.parseLong(transactionId); + modifiedTransactionId = LOAN_TRANSACTION_IDENTIFIER + transactionId; + } + final JournalEntry journalEntry = JournalEntry.createNew(office, null, account, currencyCode, modifiedTransactionId, manualEntry, + transactionDate, JournalEntryType.DEBIT, amount, null, PortfolioProductType.LOAN.getValue(), loanId, null, + loanTransactionId, null, null, null); + persistJournalEntry(journalEntry); + } + + private void createDebitJournalEntryForSavings(final Office office, final String currencyCode, final GLAccount account, + final Long savingsId, final String transactionId, final LocalDate transactionDate, final BigDecimal amount) { + final boolean manualEntry = false; + Long savingsAccountTransactionId = null; + String modifiedTransactionId = transactionId; + if (StringUtils.isNumeric(transactionId)) { + savingsAccountTransactionId = Long.parseLong(transactionId); + modifiedTransactionId = getSavingsTransactionIdentifier() + transactionId; + } + final JournalEntry journalEntry = JournalEntry.createNew(office, null, account, currencyCode, modifiedTransactionId, manualEntry, + transactionDate, JournalEntryType.DEBIT, amount, null, PortfolioProductType.SAVING.getValue(), savingsId, null, null, + savingsAccountTransactionId, null, null); + + persistJournalEntry(journalEntry); + } + + private void createDebitJournalEntryForClientPayments(final Office office, final String currencyCode, final GLAccount account, + final Long clientId, final Long transactionId, final LocalDate transactionDate, final BigDecimal amount) { + final boolean manualEntry = false; + String modifiedTransactionId = CLIENT_TRANSACTION_IDENTIFIER + transactionId; + final JournalEntry journalEntry = JournalEntry.createNew(office, null, account, currencyCode, modifiedTransactionId, manualEntry, + transactionDate, JournalEntryType.DEBIT, amount, null, PortfolioProductType.CLIENT.getValue(), clientId, null, null, null, + transactionId, null); + persistJournalEntry(journalEntry); + } + + public void createJournalEntriesForShares(final Office office, final String currencyCode, final int accountTypeToDebitId, + final int accountTypeToCreditId, final Long shareProductId, final Long paymentTypeId, final Long shareAccountId, + final String transactionId, final LocalDate transactionDate, final BigDecimal amount) { + createDebitJournalEntryForShares(office, currencyCode, accountTypeToDebitId, shareProductId, paymentTypeId, shareAccountId, + transactionId, transactionDate, amount); + createCreditJournalEntryForShares(office, currencyCode, accountTypeToCreditId, shareProductId, paymentTypeId, shareAccountId, + transactionId, transactionDate, amount); + } + + public void createDebitJournalEntryForShares(final Office office, final String currencyCode, final int accountTypeToDebitId, + final Long shareProductId, final Long paymentTypeId, final Long shareAccountId, final String transactionId, + final LocalDate transactionDate, final BigDecimal amount) { + final GLAccount debitAccount = getLinkedGLAccountForShareProduct(shareProductId, accountTypeToDebitId, paymentTypeId); + createDebitJournalEntryForShares(office, currencyCode, debitAccount, shareAccountId, transactionId, transactionDate, amount); + } + + public void createCreditJournalEntryForShares(final Office office, final String currencyCode, final int accountTypeToCreditId, + final Long shareProductId, final Long paymentTypeId, final Long shareAccountId, final String transactionId, + final LocalDate transactionDate, final BigDecimal amount) { + final GLAccount creditAccount = getLinkedGLAccountForShareProduct(shareProductId, accountTypeToCreditId, paymentTypeId); + createCreditJournalEntryForShares(office, currencyCode, creditAccount, shareAccountId, transactionId, transactionDate, amount); + } + + public void createCashBasedJournalEntriesForSharesCharges(final Office office, final String currencyCode, + final CashAccountsForShares accountTypeToBeDebited, final CashAccountsForShares accountTypeToBeCredited, + final Long shareProductId, final Long paymentTypeId, final Long shareAccountId, final String transactionId, + final LocalDate transactionDate, final BigDecimal totalAmount, final List chargePaymentDTOs) { + + createDebitJournalEntryForShares(office, currencyCode, accountTypeToBeDebited.getValue(), shareProductId, paymentTypeId, + shareAccountId, transactionId, transactionDate, totalAmount); + createCashBasedJournalEntryForSharesCharges(office, currencyCode, accountTypeToBeCredited, shareProductId, shareAccountId, + transactionId, transactionDate, totalAmount, chargePaymentDTOs); + } + + public void createCashBasedJournalEntryForSharesCharges(final Office office, final String currencyCode, + final CashAccountsForShares accountTypeToBeCredited, final Long shareProductId, final Long shareAccountId, + final String transactionId, final LocalDate transactionDate, final BigDecimal totalAmount, + final List chargePaymentDTOs) { + final Map creditDetailsMap = new LinkedHashMap<>(); + for (final ChargePaymentDTO chargePaymentDTO : chargePaymentDTOs) { + final GLAccount chargeSpecificAccount = getLinkedGLAccountForShareCharges(shareProductId, accountTypeToBeCredited.getValue(), + chargePaymentDTO.getChargeId()); + BigDecimal chargeSpecificAmount = chargePaymentDTO.getAmount(); + + // adjust net credit amount if the account is already present in the + // map + if (creditDetailsMap.containsKey(chargeSpecificAccount)) { + final BigDecimal existingAmount = creditDetailsMap.get(chargeSpecificAccount); + chargeSpecificAmount = chargeSpecificAmount.add(existingAmount); + } + creditDetailsMap.put(chargeSpecificAccount, chargeSpecificAmount); + } + + BigDecimal totalCreditedAmount = BigDecimal.ZERO; + for (final Map.Entry entry : creditDetailsMap.entrySet()) { + final GLAccount account = entry.getKey(); + final BigDecimal amount = entry.getValue(); + totalCreditedAmount = totalCreditedAmount.add(amount); + createCreditJournalEntryForShares(office, currencyCode, account, shareAccountId, transactionId, transactionDate, amount); + } + if (totalAmount.compareTo(totalCreditedAmount) != 0) { + throw new PlatformDataIntegrityException("Recent Portfolio changes w.r.t Charges for shares have Broken the accounting code", + "Recent Portfolio changes w.r.t Charges for shares have Broken the accounting code"); + } + } + + public void revertCashBasedJournalEntryForSharesCharges(final Office office, final String currencyCode, + final CashAccountsForShares accountTypeToBeCredited, final Long shareProductId, final Long shareAccountId, + final String transactionId, final LocalDate transactionDate, final BigDecimal totalAmount, + final List chargePaymentDTOs) { + final Map creditDetailsMap = new LinkedHashMap<>(); + for (final ChargePaymentDTO chargePaymentDTO : chargePaymentDTOs) { + final GLAccount chargeSpecificAccount = getLinkedGLAccountForShareCharges(shareProductId, accountTypeToBeCredited.getValue(), + chargePaymentDTO.getChargeId()); + BigDecimal chargeSpecificAmount = chargePaymentDTO.getAmount(); + + // adjust net credit amount if the account is already present in the + // map + if (creditDetailsMap.containsKey(chargeSpecificAccount)) { + final BigDecimal existingAmount = creditDetailsMap.get(chargeSpecificAccount); + chargeSpecificAmount = chargeSpecificAmount.add(existingAmount); + } + creditDetailsMap.put(chargeSpecificAccount, chargeSpecificAmount); + } + + BigDecimal totalCreditedAmount = BigDecimal.ZERO; + for (final Map.Entry entry : creditDetailsMap.entrySet()) { + final GLAccount account = entry.getKey(); + final BigDecimal amount = entry.getValue(); + totalCreditedAmount = totalCreditedAmount.add(amount); + createDebitJournalEntryForShares(office, currencyCode, account, shareAccountId, transactionId, transactionDate, amount); + } + if (totalAmount.compareTo(totalCreditedAmount) != 0) { + throw new PlatformDataIntegrityException("Recent Portfolio changes w.r.t Charges for shares have Broken the accounting code", + "Recent Portfolio changes w.r.t Charges for shares have Broken the accounting code"); + } + } + + private void createDebitJournalEntryForShares(final Office office, final String currencyCode, final GLAccount account, + final Long shareAccountId, final String transactionId, final LocalDate transactionDate, final BigDecimal amount) { + final boolean manualEntry = false; + Long shareTransactionId = null; + String modifiedTransactionId = transactionId; + if (StringUtils.isNumeric(transactionId)) { + shareTransactionId = Long.parseLong(transactionId); + modifiedTransactionId = SHARE_TRANSACTION_IDENTIFIER + transactionId; + } + final JournalEntry journalEntry = JournalEntry.createNew(office, null, account, currencyCode, modifiedTransactionId, manualEntry, + transactionDate, JournalEntryType.DEBIT, amount, null, PortfolioProductType.SHARES.getValue(), shareAccountId, null, null, + null, null, shareTransactionId); + persistJournalEntry(journalEntry); + } + + private void createCreditJournalEntryForShares(final Office office, final String currencyCode, final GLAccount account, + final Long shareAccountId, final String transactionId, final LocalDate transactionDate, final BigDecimal amount) { + final boolean manualEntry = false; + Long shareTransactionId = null; + String modifiedTransactionId = transactionId; + if (StringUtils.isNumeric(transactionId)) { + shareTransactionId = Long.parseLong(transactionId); + modifiedTransactionId = SHARE_TRANSACTION_IDENTIFIER + transactionId; + } + final JournalEntry journalEntry = JournalEntry.createNew(office, null, account, currencyCode, modifiedTransactionId, manualEntry, + transactionDate, JournalEntryType.CREDIT, amount, null, PortfolioProductType.SHARES.getValue(), shareAccountId, null, null, + null, null, shareTransactionId); + persistJournalEntry(journalEntry); + } + + public GLAccount getLinkedGLAccountForLoanProduct(final Long loanProductId, final int accountMappingTypeId, final Long paymentTypeId) { + GLAccount glAccount; + if (isOrganizationAccount(accountMappingTypeId)) { + FinancialActivityAccount financialActivityAccount = this.financialActivityAccountRepository + .findByFinancialActivityTypeWithNotFoundDetection(accountMappingTypeId); + glAccount = financialActivityAccount.getGlAccount(); + } else { + ProductToGLAccountMapping accountMapping = this.accountMappingRepository.findCoreProductToFinAccountMapping(loanProductId, + PortfolioProductType.LOAN.getValue(), accountMappingTypeId); + + /**** + * Get more specific mapping for FUND source accounts (based on payment channels). Note that fund source + * placeholder ID would be same for both cash and accrual accounts + ***/ + if (accountMappingTypeId == CashAccountsForLoan.FUND_SOURCE.getValue()) { + final ProductToGLAccountMapping paymentChannelSpecificAccountMapping = this.accountMappingRepository + .findByProductIdAndProductTypeAndFinancialAccountTypeAndPaymentTypeId(loanProductId, + PortfolioProductType.LOAN.getValue(), accountMappingTypeId, paymentTypeId); + if (paymentChannelSpecificAccountMapping != null) { + accountMapping = paymentChannelSpecificAccountMapping; + } + } + + if (accountMapping == null) { + throw new ProductToGLAccountMappingNotFoundException(PortfolioProductType.LOAN, loanProductId, + AccrualAccountsForLoan.fromInt(accountMappingTypeId).toString()); + + } + glAccount = accountMapping.getGlAccount(); + } + return glAccount; + } + + private GLAccount getLinkedGLAccountForLoanCharges(final Long loanProductId, final int accountMappingTypeId, final Long chargeId) { + ProductToGLAccountMapping accountMapping = this.accountMappingRepository.findCoreProductToFinAccountMapping(loanProductId, + PortfolioProductType.LOAN.getValue(), accountMappingTypeId); + /***** + * Get more specific mappings for Charges and penalties (based on the actual charge /penalty coupled with the + * loan product). Note the income from fees and income from penalties placeholder ID would be the same for both + * cash and accrual based accounts + *****/ + + // Check for charge-specific mappings for all account types (not just income accounts) + // This allows charge-specific GL account mappings for debit accounts as well + if (chargeId != null) { + final ProductToGLAccountMapping chargeSpecificAccountMapping = this.accountMappingRepository + .findProductIdAndProductTypeAndFinancialAccountTypeAndChargeId(loanProductId, PortfolioProductType.LOAN.getValue(), + accountMappingTypeId, chargeId); + if (chargeSpecificAccountMapping != null) { + accountMapping = chargeSpecificAccountMapping; + } + } + return accountMapping.getGlAccount(); + } + + private GLAccount getLinkedGLAccountForSavingsCharges(final Long savingsProductId, final int accountMappingTypeId, + final Long chargeId) { + + ProductToGLAccountMapping accountMapping = this.accountMappingRepository.findCoreProductToFinAccountMapping(savingsProductId, + PortfolioProductType.SAVING.getValue(), accountMappingTypeId); + /***** + * Get more specific mappings for Charges and penalties (based on the actual charge /penalty coupled with the + * loan product). Note the income from fees and income from penalties placeholder ID would be the same for both + * cash and accrual based accounts + *****/ + + // Vishwas TODO: remove this condition as it should always be true + + if (accountMappingTypeId == CashAccountsForSavings.INCOME_FROM_FEES.getValue() + || accountMappingTypeId == CashAccountsForLoan.INCOME_FROM_PENALTIES.getValue()) { + GLAccount glAccount = chargeRepositoryWrapper.findOneWithNotFoundDetection(chargeId).getAccount(); + if (glAccount != null) { + return glAccount; + } + final ProductToGLAccountMapping chargeSpecificIncomeAccountMapping = this.accountMappingRepository + .findProductIdAndProductTypeAndFinancialAccountTypeAndChargeId(savingsProductId, PortfolioProductType.SAVING.getValue(), + accountMappingTypeId, chargeId); + if (chargeSpecificIncomeAccountMapping != null) { + + accountMapping = chargeSpecificIncomeAccountMapping; + } + } + + return accountMapping.getGlAccount(); + } + + private GLAccount getLinkedGLAccountForSavingsProduct(final Long savingsProductId, final int accountMappingTypeId, + final Long paymentTypeId) { + GLAccount glAccount; + if (isOrganizationAccount(accountMappingTypeId)) { + FinancialActivityAccount financialActivityAccount = this.financialActivityAccountRepository + .findByFinancialActivityTypeWithNotFoundDetection(accountMappingTypeId); + glAccount = financialActivityAccount.getGlAccount(); + } else { + ProductToGLAccountMapping accountMapping = this.accountMappingRepository.findCoreProductToFinAccountMapping(savingsProductId, + PortfolioProductType.SAVING.getValue(), accountMappingTypeId); + /**** + * Get more specific mapping for FUND source accounts (based on payment channels). Note that fund source + * placeholder ID would be same for both cash and accrual accounts + ***/ + if (accountMappingTypeId == CashAccountsForSavings.SAVINGS_REFERENCE.getValue()) { + final ProductToGLAccountMapping paymentChannelSpecificAccountMapping = this.accountMappingRepository + .findByProductIdAndProductTypeAndFinancialAccountTypeAndPaymentTypeId(savingsProductId, + PortfolioProductType.SAVING.getValue(), accountMappingTypeId, paymentTypeId); + if (paymentChannelSpecificAccountMapping != null) { + accountMapping = paymentChannelSpecificAccountMapping; + } + } + glAccount = accountMapping.getGlAccount(); + } + return glAccount; + } + + private GLAccount getLinkedGLAccountForShareProduct(final Long shareProductId, final int accountMappingTypeId, + final Long paymentTypeId) { + GLAccount glAccount; + if (isOrganizationAccount(accountMappingTypeId)) { + FinancialActivityAccount financialActivityAccount = this.financialActivityAccountRepository + .findByFinancialActivityTypeWithNotFoundDetection(accountMappingTypeId); + glAccount = financialActivityAccount.getGlAccount(); + } else { + ProductToGLAccountMapping accountMapping = this.accountMappingRepository.findCoreProductToFinAccountMapping(shareProductId, + PortfolioProductType.SHARES.getValue(), accountMappingTypeId); + + if (accountMappingTypeId == CashAccountsForShares.SHARES_REFERENCE.getValue()) { + final ProductToGLAccountMapping paymentChannelSpecificAccountMapping = this.accountMappingRepository + .findByProductIdAndProductTypeAndFinancialAccountTypeAndPaymentTypeId(shareProductId, + PortfolioProductType.SHARES.getValue(), accountMappingTypeId, paymentTypeId); + if (paymentChannelSpecificAccountMapping != null) { + accountMapping = paymentChannelSpecificAccountMapping; + } + } + glAccount = accountMapping.getGlAccount(); + } + return glAccount; + } + + private GLAccount getLinkedGLAccountForShareCharges(final Long shareProductId, final int accountMappingTypeId, final Long chargeId) { + ProductToGLAccountMapping accountMapping = this.accountMappingRepository.findCoreProductToFinAccountMapping(shareProductId, + PortfolioProductType.SHARES.getValue(), accountMappingTypeId); + /***** + * Get more specific mappings for Charges and penalties (based on the actual charge /penalty coupled with the + * loan product). Note the income from fees and income from penalties placeholder ID would be the same for both + * cash and accrual based accounts + *****/ + + final ProductToGLAccountMapping chargeSpecificIncomeAccountMapping = this.accountMappingRepository + .findProductIdAndProductTypeAndFinancialAccountTypeAndChargeId(shareProductId, PortfolioProductType.SHARES.getValue(), + accountMappingTypeId, chargeId); + if (chargeSpecificIncomeAccountMapping != null) { + accountMapping = chargeSpecificIncomeAccountMapping; + } + return accountMapping.getGlAccount(); + } + + private boolean isOrganizationAccount(final int accountMappingTypeId) { + return FinancialActivity.fromInt(accountMappingTypeId) != null; + } + + public BigDecimal createCreditJournalEntryOrReversalForClientPayments(final Office office, final String currencyCode, + final Long clientId, final Long transactionId, final LocalDate transactionDate, final Boolean isReversal, + final List clientChargePaymentDTOs) { + /*** + * Map to track each account affected and the net credit to be made for a particular account + ***/ + final Map creditDetailsMap = new LinkedHashMap<>(); + for (final ClientChargePaymentDTO clientChargePaymentDTO : clientChargePaymentDTOs) { + if (clientChargePaymentDTO.getIncomeAccountId() != null) { + final Long accountId = clientChargePaymentDTO.getIncomeAccountId(); + BigDecimal chargeSpecificAmount = clientChargePaymentDTO.getAmount(); + + // adjust net credit amount if the account is already present in + // the map + if (creditDetailsMap.containsKey(accountId)) { + final BigDecimal existingAmount = creditDetailsMap.get(accountId); + chargeSpecificAmount = chargeSpecificAmount.add(existingAmount); + } + creditDetailsMap.put(accountId, chargeSpecificAmount); + } + } + + BigDecimal totalCreditedAmount = BigDecimal.ZERO; + for (final Map.Entry entry : creditDetailsMap.entrySet()) { + final GLAccount account = getGLAccountById(entry.getKey()); + final BigDecimal amount = entry.getValue(); + totalCreditedAmount = totalCreditedAmount.add(amount); + if (isReversal) { + createDebitJournalEntryForClientPayments(office, currencyCode, account, clientId, transactionId, transactionDate, amount); + } else { + createCreditJournalEntryForClientPayments(office, currencyCode, account, clientId, transactionId, transactionDate, amount); + } + } + return totalCreditedAmount; + } + + public void createDebitJournalEntryOrReversalForClientChargePayments(final Office office, final String currencyCode, + final Long clientId, final Long transactionId, final LocalDate transactionDate, final BigDecimal amount, + final Boolean isReversal) { + final GLAccount account = financialActivityAccountRepository + .findByFinancialActivityTypeWithNotFoundDetection(FinancialActivity.ASSET_FUND_SOURCE.getValue()).getGlAccount(); + if (isReversal) { + createCreditJournalEntryForClientPayments(office, currencyCode, account, clientId, transactionId, transactionDate, amount); + } else { + createDebitJournalEntryForClientPayments(office, currencyCode, account, clientId, transactionId, transactionDate, amount); + } + } + + private GLAccount getGLAccountById(final Long accountId) { + return this.glAccountRepository.getReferenceById(accountId); + } + + public Integer getValueForFeeOrPenaltyIncomeAccount(final String chargeRefundChargeType) { + if (chargeRefundChargeType == null + || !(chargeRefundChargeType.equalsIgnoreCase("P") || chargeRefundChargeType.equalsIgnoreCase("F"))) { + String errorValue; + errorValue = Objects.requireNonNullElse(chargeRefundChargeType, "Null"); + throw new PlatformDataIntegrityException("error.msg.chargeRefundChargeType.can.only.be.P.or.F", + "chargeRefundChargeType can only be P (Penalty) or F(Fee) - Value is: " + errorValue); + } + Integer incomeAccount; + if (chargeRefundChargeType.equalsIgnoreCase("P")) { + incomeAccount = AccrualAccountsForLoan.INCOME_FROM_PENALTIES.getValue(); + } else { + incomeAccount = AccrualAccountsForLoan.INCOME_FROM_FEES.getValue(); + + } + return incomeAccount; + } + + public JournalEntry persistJournalEntry(JournalEntry journalEntry) { + boolean isNew = journalEntry.isNew(); + JournalEntry savedJournalEntry = this.glJournalEntryRepository.saveAndFlush(journalEntry); + if (isNew && journalEntry.getLoanTransactionId() != null) { + businessEventNotifierService.notifyPostBusinessEvent(new LoanJournalEntryCreatedBusinessEvent(savedJournalEntry)); + } + return savedJournalEntry; + } + + private void createJournalEntriesForLoanChargesInternal(final Office office, final String currencyCode, final int accountMappingTypeId, + final Long loanProductId, final Long loanId, final String transactionId, final LocalDate transactionDate, + final BigDecimal totalAmount, final List chargePaymentDTOs, final boolean isCredit) { + final Map creditDetailsMap = new LinkedHashMap<>(); + + for (final ChargePaymentDTO chargePaymentDTO : chargePaymentDTOs) { + final Long chargeId = chargePaymentDTO.getChargeId(); + final GLAccount account = getLinkedGLAccountForLoanCharges(loanProductId, accountMappingTypeId, chargeId); + BigDecimal amount = chargePaymentDTO.getAmount(); + + creditDetailsMap.merge(account, amount, BigDecimal::add); + } + + BigDecimal totalCreditedAmount = BigDecimal.ZERO; + + for (Map.Entry entry : creditDetailsMap.entrySet()) { + GLAccount account = entry.getKey(); + BigDecimal amount = entry.getValue(); + totalCreditedAmount = totalCreditedAmount.add(amount); + + if (isCredit) { + createCreditJournalEntryForLoan(office, currencyCode, account, loanId, transactionId, transactionDate, amount); + } else { + createDebitJournalEntryForLoan(office, currencyCode, account, loanId, transactionId, transactionDate, amount); + } + } + + if (totalAmount.compareTo(totalCreditedAmount) != 0) { + throw new PlatformDataIntegrityException( + "Meltdown in advanced accounting...sum of all charges is not equal to the fee charge for a transaction", + "Meltdown in advanced accounting...sum of all charges is not equal to the fee charge for a transaction", + totalCreditedAmount, totalAmount); + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForSavings.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForSavings.java index e0b737073..748daf5d0 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForSavings.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForSavings.java @@ -1,285 +1,290 @@ -/** - * 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.accounting.journalentry.service; - -import java.math.BigDecimal; -import java.time.LocalDate; -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.apache.fineract.accounting.closure.domain.GLClosure; -import org.apache.fineract.accounting.common.AccountingConstants.AccrualAccountsForSavings; -import org.apache.fineract.accounting.common.AccountingConstants.FinancialActivity; -import org.apache.fineract.accounting.journalentry.data.ChargePaymentDTO; -import org.apache.fineract.accounting.journalentry.data.SavingsDTO; -import org.apache.fineract.accounting.journalentry.data.SavingsTransactionDTO; -import org.apache.fineract.infrastructure.core.service.MathUtil; -import org.apache.fineract.organisation.office.domain.Office; -import org.springframework.stereotype.Component; - -@Component -@RequiredArgsConstructor -public class AccrualBasedAccountingProcessorForSavings implements AccountingProcessorForSavings { - - private final AccountingProcessorHelper helper; - - @Override - public void createJournalEntriesForSavings(final SavingsDTO savingsDTO) { - final GLClosure latestGLClosure = this.helper.getLatestClosureByBranch(savingsDTO.getOfficeId()); - final Long savingsProductId = savingsDTO.getSavingsProductId(); - final Long savingsId = savingsDTO.getSavingsId(); - final String currencyCode = savingsDTO.getCurrencyCode(); - for (final SavingsTransactionDTO savingsTransactionDTO : savingsDTO.getNewSavingsTransactions()) { - final LocalDate transactionDate = savingsTransactionDTO.getTransactionDate(); - final String transactionId = savingsTransactionDTO.getTransactionId(); - final Office office = this.helper.getOfficeById(savingsTransactionDTO.getOfficeId()); - final Long paymentTypeId = savingsTransactionDTO.getPaymentTypeId(); - final boolean isReversal = savingsTransactionDTO.isReversed(); - final BigDecimal amount = savingsTransactionDTO.getAmount(); - final BigDecimal overdraftAmount = savingsTransactionDTO.getOverdraftAmount(); - final List feePayments = savingsTransactionDTO.getFeePayments(); - final List penaltyPayments = savingsTransactionDTO.getPenaltyPayments(); - - this.helper.checkForBranchClosures(latestGLClosure, transactionDate); - - if (savingsTransactionDTO.getTransactionType().isWithdrawal() && savingsTransactionDTO.isOverdraftTransaction()) { - boolean isPositive = amount.subtract(overdraftAmount).compareTo(BigDecimal.ZERO) > 0; - if (savingsTransactionDTO.isAccountTransfer()) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, - AccrualAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), - FinancialActivity.LIABILITY_TRANSFER.getValue(), savingsProductId, paymentTypeId, savingsId, transactionId, - transactionDate, overdraftAmount, isReversal); - if (isPositive) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, - AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), FinancialActivity.LIABILITY_TRANSFER.getValue(), - savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, - amount.subtract(overdraftAmount), isReversal); - } - } else { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, - AccrualAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), - AccrualAccountsForSavings.SAVINGS_REFERENCE.getValue(), savingsProductId, paymentTypeId, savingsId, - transactionId, transactionDate, overdraftAmount, isReversal); - if (isPositive) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, - AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), - AccrualAccountsForSavings.SAVINGS_REFERENCE.getValue(), savingsProductId, paymentTypeId, savingsId, - transactionId, transactionDate, amount.subtract(overdraftAmount), isReversal); - } - } - } - - else if (savingsTransactionDTO.getTransactionType().isDeposit() && savingsTransactionDTO.isOverdraftTransaction()) { - boolean isPositive = amount.subtract(overdraftAmount).compareTo(BigDecimal.ZERO) > 0; - if (savingsTransactionDTO.isAccountTransfer()) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, - FinancialActivity.LIABILITY_TRANSFER.getValue(), - AccrualAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), savingsProductId, paymentTypeId, savingsId, - transactionId, transactionDate, overdraftAmount, isReversal); - if (isPositive) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, - FinancialActivity.LIABILITY_TRANSFER.getValue(), AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), - savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, - amount.subtract(overdraftAmount), isReversal); - } - } else { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, - AccrualAccountsForSavings.SAVINGS_REFERENCE.getValue(), - AccrualAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), savingsProductId, paymentTypeId, savingsId, - transactionId, transactionDate, overdraftAmount, isReversal); - if (isPositive) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, - AccrualAccountsForSavings.SAVINGS_REFERENCE.getValue(), - AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), savingsProductId, paymentTypeId, savingsId, - transactionId, transactionDate, amount.subtract(overdraftAmount), isReversal); - } - } - } - - /** Handle Deposits and reversals of deposits **/ - else if (savingsTransactionDTO.getTransactionType().isDeposit()) { - if (savingsTransactionDTO.isAccountTransfer()) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, - FinancialActivity.LIABILITY_TRANSFER.getValue(), AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), - savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); - } else { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, - AccrualAccountsForSavings.SAVINGS_REFERENCE.getValue(), AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), - savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); - } - } - - /** Handle Deposits and reversals of Dividend pay outs **/ - else if (savingsTransactionDTO.getTransactionType().isDividendPayout()) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, - FinancialActivity.PAYABLE_DIVIDENDS.getValue(), AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), - savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); - } - - /** Handle withdrawals and reversals of withdrawals **/ - else if (savingsTransactionDTO.getTransactionType().isWithdrawal()) { - if (savingsTransactionDTO.isAccountTransfer()) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, - AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), FinancialActivity.LIABILITY_TRANSFER.getValue(), - savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); - } else { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, - AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), AccrualAccountsForSavings.SAVINGS_REFERENCE.getValue(), - savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); - } - } - - else if (savingsTransactionDTO.getTransactionType().isEscheat()) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, - AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), AccrualAccountsForSavings.ESCHEAT_LIABILITY.getValue(), - savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); - } - /** - * Handle Interest Applications and reversals of Interest Applications - **/ - else if (savingsTransactionDTO.getTransactionType().isInterestPosting() && savingsTransactionDTO.isOverdraftTransaction()) { - boolean isPositive = amount.subtract(overdraftAmount).compareTo(BigDecimal.ZERO) > 0; - // Post journal entry if earned interest amount is greater than - // zero - if (savingsTransactionDTO.getAmount().compareTo(BigDecimal.ZERO) > 0) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, - AccrualAccountsForSavings.INTEREST_ON_SAVINGS.getValue(), - AccrualAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), savingsProductId, paymentTypeId, savingsId, - transactionId, transactionDate, overdraftAmount, isReversal); - if (isPositive) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, - AccrualAccountsForSavings.INTEREST_ON_SAVINGS.getValue(), - AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), savingsProductId, paymentTypeId, savingsId, - transactionId, transactionDate, amount.subtract(overdraftAmount), isReversal); - } - } - } - - else if (savingsTransactionDTO.getTransactionType().isInterestPosting()) { - // Post journal entry if earned interest amount is greater than - // zero - if (savingsTransactionDTO.getAmount().compareTo(BigDecimal.ZERO) > 0) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, - AccrualAccountsForSavings.INTEREST_PAYABLE.getValue(), AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), - savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); - } - } - - else if (savingsTransactionDTO.getTransactionType().isAccrual()) { - // Post journal entry for Accrual Recognition - if (savingsTransactionDTO.getAmount().compareTo(BigDecimal.ZERO) > 0) { - if (MathUtil.isGreaterThanZero(overdraftAmount)) { - this.helper.createAccrualBasedDebitJournalEntriesAndReversalsForSavings(office, currencyCode, - AccrualAccountsForSavings.INTEREST_ON_SAVINGS.getValue(), savingsProductId, paymentTypeId, savingsId, - transactionId, transactionDate, amount, isReversal); - this.helper.createAccrualBasedCreditJournalEntriesAndReversalsForSavings(office, currencyCode, - AccrualAccountsForSavings.INTEREST_PAYABLE.getValue(), savingsProductId, paymentTypeId, savingsId, - transactionId, transactionDate, amount, isReversal); - } else { - this.helper.createAccrualBasedDebitJournalEntriesAndReversalsForSavings(office, currencyCode, - AccrualAccountsForSavings.INTEREST_RECEIVABLE.getValue(), savingsProductId, paymentTypeId, savingsId, - transactionId, transactionDate, amount, isReversal); - this.helper.createAccrualBasedCreditJournalEntriesAndReversalsForSavings(office, currencyCode, - AccrualAccountsForSavings.INCOME_FROM_INTEREST.getValue(), savingsProductId, paymentTypeId, savingsId, - transactionId, transactionDate, amount, isReversal); - } - } - } - - else if (savingsTransactionDTO.getTransactionType().isWithholdTax()) { - this.helper.createAccrualBasedJournalEntriesAndReversalsForSavingsTax(office, currencyCode, - AccrualAccountsForSavings.SAVINGS_CONTROL, AccrualAccountsForSavings.SAVINGS_REFERENCE, savingsProductId, - paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal, - savingsTransactionDTO.getTaxPayments()); - } - - /** Handle Fees Deductions and reversals of Fees Deductions **/ - else if (savingsTransactionDTO.getTransactionType().isFeeDeduction() && savingsTransactionDTO.isOverdraftTransaction()) { - boolean isPositive = amount.subtract(overdraftAmount).compareTo(BigDecimal.ZERO) > 0; - // Is the Charge a penalty? - if (penaltyPayments.size() > 0) { - this.helper.createAccrualBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, - AccrualAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL, AccrualAccountsForSavings.INCOME_FROM_PENALTIES, - savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, overdraftAmount, isReversal, - penaltyPayments); - if (isPositive) { - this.helper.createAccrualBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, - AccrualAccountsForSavings.SAVINGS_CONTROL, AccrualAccountsForSavings.INCOME_FROM_PENALTIES, - savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, - amount.subtract(overdraftAmount), isReversal, penaltyPayments); - } - } else { - this.helper.createAccrualBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, - AccrualAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL, AccrualAccountsForSavings.INCOME_FROM_FEES, - savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, overdraftAmount, isReversal, - feePayments); - if (isPositive) { - this.helper.createAccrualBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, - AccrualAccountsForSavings.SAVINGS_CONTROL, AccrualAccountsForSavings.INCOME_FROM_FEES, savingsProductId, - paymentTypeId, savingsId, transactionId, transactionDate, amount.subtract(overdraftAmount), isReversal, - feePayments); - } - } - } - - else if (savingsTransactionDTO.getTransactionType().isFeeDeduction()) { - // Is the Charge a penalty? - if (penaltyPayments.size() > 0) { - this.helper.createAccrualBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, - AccrualAccountsForSavings.SAVINGS_CONTROL, AccrualAccountsForSavings.INCOME_FROM_PENALTIES, savingsProductId, - paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal, penaltyPayments); - } else { - this.helper.createAccrualBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, - AccrualAccountsForSavings.SAVINGS_CONTROL, AccrualAccountsForSavings.INCOME_FROM_FEES, savingsProductId, - paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal, feePayments); - } - } - - /** Handle Transfers proposal **/ - else if (savingsTransactionDTO.getTransactionType().isInitiateTransfer()) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, - AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), AccrualAccountsForSavings.TRANSFERS_SUSPENSE.getValue(), - savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); - } - - /** Handle Transfer Withdrawal or Acceptance **/ - else if (savingsTransactionDTO.getTransactionType().isWithdrawTransfer() - || savingsTransactionDTO.getTransactionType().isApproveTransfer()) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, - AccrualAccountsForSavings.TRANSFERS_SUSPENSE.getValue(), AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), - savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); - } - - /** overdraft **/ - else if (savingsTransactionDTO.getTransactionType().isOverdraftInterest()) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, - AccrualAccountsForSavings.SAVINGS_REFERENCE.getValue(), AccrualAccountsForSavings.INCOME_FROM_INTEREST.getValue(), - savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); - } else if (savingsTransactionDTO.getTransactionType().isWrittenoff()) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, - AccrualAccountsForSavings.LOSSES_WRITTEN_OFF.getValue(), - AccrualAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), savingsProductId, paymentTypeId, savingsId, - transactionId, transactionDate, amount, isReversal); - } else if (savingsTransactionDTO.getTransactionType().isOverdraftFee()) { - this.helper.createAccrualBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, - AccrualAccountsForSavings.SAVINGS_REFERENCE, AccrualAccountsForSavings.INCOME_FROM_FEES, savingsProductId, - paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal, feePayments); - } - } - } -} +/** + * 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.accounting.journalentry.service; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.accounting.closure.domain.GLClosure; +import org.apache.fineract.accounting.common.AccountingConstants.AccrualAccountsForSavings; +import org.apache.fineract.accounting.common.AccountingConstants.FinancialActivity; +import org.apache.fineract.accounting.journalentry.data.ChargePaymentDTO; +import org.apache.fineract.accounting.journalentry.data.SavingsDTO; +import org.apache.fineract.accounting.journalentry.data.SavingsTransactionDTO; +import org.apache.fineract.infrastructure.core.service.MathUtil; +import org.apache.fineract.organisation.office.domain.Office; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class AccrualBasedAccountingProcessorForSavings implements AccountingProcessorForSavings { + + private final AccountingProcessorHelper helper; + + @Override + public void createJournalEntriesForSavings(final SavingsDTO savingsDTO) { + AccountingProcessorHelper.setDepositAccountTypeContext(savingsDTO.getDepositAccountType()); + try { + final GLClosure latestGLClosure = this.helper.getLatestClosureByBranch(savingsDTO.getOfficeId()); + final Long savingsProductId = savingsDTO.getSavingsProductId(); + final Long savingsId = savingsDTO.getSavingsId(); + final String currencyCode = savingsDTO.getCurrencyCode(); + for (final SavingsTransactionDTO savingsTransactionDTO : savingsDTO.getNewSavingsTransactions()) { + final LocalDate transactionDate = savingsTransactionDTO.getTransactionDate(); + final String transactionId = savingsTransactionDTO.getTransactionId(); + final Office office = this.helper.getOfficeById(savingsTransactionDTO.getOfficeId()); + final Long paymentTypeId = savingsTransactionDTO.getPaymentTypeId(); + final boolean isReversal = savingsTransactionDTO.isReversed(); + final BigDecimal amount = savingsTransactionDTO.getAmount(); + final BigDecimal overdraftAmount = savingsTransactionDTO.getOverdraftAmount(); + final List feePayments = savingsTransactionDTO.getFeePayments(); + final List penaltyPayments = savingsTransactionDTO.getPenaltyPayments(); + + this.helper.checkForBranchClosures(latestGLClosure, transactionDate); + + if (savingsTransactionDTO.getTransactionType().isWithdrawal() && savingsTransactionDTO.isOverdraftTransaction()) { + boolean isPositive = amount.subtract(overdraftAmount).compareTo(BigDecimal.ZERO) > 0; + if (savingsTransactionDTO.isAccountTransfer()) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + AccrualAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), + FinancialActivity.LIABILITY_TRANSFER.getValue(), savingsProductId, paymentTypeId, savingsId, transactionId, + transactionDate, overdraftAmount, isReversal); + if (isPositive) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), FinancialActivity.LIABILITY_TRANSFER.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, + amount.subtract(overdraftAmount), isReversal); + } + } else { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + AccrualAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), + AccrualAccountsForSavings.SAVINGS_REFERENCE.getValue(), savingsProductId, paymentTypeId, savingsId, + transactionId, transactionDate, overdraftAmount, isReversal); + if (isPositive) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), + AccrualAccountsForSavings.SAVINGS_REFERENCE.getValue(), savingsProductId, paymentTypeId, savingsId, + transactionId, transactionDate, amount.subtract(overdraftAmount), isReversal); + } + } + } + + else if (savingsTransactionDTO.getTransactionType().isDeposit() && savingsTransactionDTO.isOverdraftTransaction()) { + boolean isPositive = amount.subtract(overdraftAmount).compareTo(BigDecimal.ZERO) > 0; + if (savingsTransactionDTO.isAccountTransfer()) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + FinancialActivity.LIABILITY_TRANSFER.getValue(), + AccrualAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), savingsProductId, paymentTypeId, savingsId, + transactionId, transactionDate, overdraftAmount, isReversal); + if (isPositive) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + FinancialActivity.LIABILITY_TRANSFER.getValue(), AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, + amount.subtract(overdraftAmount), isReversal); + } + } else { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + AccrualAccountsForSavings.SAVINGS_REFERENCE.getValue(), + AccrualAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), savingsProductId, paymentTypeId, savingsId, + transactionId, transactionDate, overdraftAmount, isReversal); + if (isPositive) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + AccrualAccountsForSavings.SAVINGS_REFERENCE.getValue(), + AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), savingsProductId, paymentTypeId, savingsId, + transactionId, transactionDate, amount.subtract(overdraftAmount), isReversal); + } + } + } + + /** Handle Deposits and reversals of deposits **/ + else if (savingsTransactionDTO.getTransactionType().isDeposit()) { + if (savingsTransactionDTO.isAccountTransfer()) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + FinancialActivity.LIABILITY_TRANSFER.getValue(), AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); + } else { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + AccrualAccountsForSavings.SAVINGS_REFERENCE.getValue(), AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); + } + } + + /** Handle Deposits and reversals of Dividend pay outs **/ + else if (savingsTransactionDTO.getTransactionType().isDividendPayout()) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + FinancialActivity.PAYABLE_DIVIDENDS.getValue(), AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); + } + + /** Handle withdrawals and reversals of withdrawals **/ + else if (savingsTransactionDTO.getTransactionType().isWithdrawal()) { + if (savingsTransactionDTO.isAccountTransfer()) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), FinancialActivity.LIABILITY_TRANSFER.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); + } else { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), AccrualAccountsForSavings.SAVINGS_REFERENCE.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); + } + } + + else if (savingsTransactionDTO.getTransactionType().isEscheat()) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), AccrualAccountsForSavings.ESCHEAT_LIABILITY.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); + } + /** + * Handle Interest Applications and reversals of Interest Applications + **/ + else if (savingsTransactionDTO.getTransactionType().isInterestPosting() && savingsTransactionDTO.isOverdraftTransaction()) { + boolean isPositive = amount.subtract(overdraftAmount).compareTo(BigDecimal.ZERO) > 0; + // Post journal entry if earned interest amount is greater than + // zero + if (savingsTransactionDTO.getAmount().compareTo(BigDecimal.ZERO) > 0) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + AccrualAccountsForSavings.INTEREST_ON_SAVINGS.getValue(), + AccrualAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), savingsProductId, paymentTypeId, savingsId, + transactionId, transactionDate, overdraftAmount, isReversal); + if (isPositive) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + AccrualAccountsForSavings.INTEREST_ON_SAVINGS.getValue(), + AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), savingsProductId, paymentTypeId, savingsId, + transactionId, transactionDate, amount.subtract(overdraftAmount), isReversal); + } + } + } + + else if (savingsTransactionDTO.getTransactionType().isInterestPosting()) { + // Post journal entry if earned interest amount is greater than + // zero + if (savingsTransactionDTO.getAmount().compareTo(BigDecimal.ZERO) > 0) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + AccrualAccountsForSavings.INTEREST_PAYABLE.getValue(), AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); + } + } + + else if (savingsTransactionDTO.getTransactionType().isAccrual()) { + // Post journal entry for Accrual Recognition + if (savingsTransactionDTO.getAmount().compareTo(BigDecimal.ZERO) > 0) { + if (MathUtil.isGreaterThanZero(overdraftAmount)) { + this.helper.createAccrualBasedDebitJournalEntriesAndReversalsForSavings(office, currencyCode, + AccrualAccountsForSavings.INTEREST_ON_SAVINGS.getValue(), savingsProductId, paymentTypeId, savingsId, + transactionId, transactionDate, amount, isReversal); + this.helper.createAccrualBasedCreditJournalEntriesAndReversalsForSavings(office, currencyCode, + AccrualAccountsForSavings.INTEREST_PAYABLE.getValue(), savingsProductId, paymentTypeId, savingsId, + transactionId, transactionDate, amount, isReversal); + } else { + this.helper.createAccrualBasedDebitJournalEntriesAndReversalsForSavings(office, currencyCode, + AccrualAccountsForSavings.INTEREST_RECEIVABLE.getValue(), savingsProductId, paymentTypeId, savingsId, + transactionId, transactionDate, amount, isReversal); + this.helper.createAccrualBasedCreditJournalEntriesAndReversalsForSavings(office, currencyCode, + AccrualAccountsForSavings.INCOME_FROM_INTEREST.getValue(), savingsProductId, paymentTypeId, savingsId, + transactionId, transactionDate, amount, isReversal); + } + } + } + + else if (savingsTransactionDTO.getTransactionType().isWithholdTax()) { + this.helper.createAccrualBasedJournalEntriesAndReversalsForSavingsTax(office, currencyCode, + AccrualAccountsForSavings.SAVINGS_CONTROL, AccrualAccountsForSavings.SAVINGS_REFERENCE, savingsProductId, + paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal, + savingsTransactionDTO.getTaxPayments()); + } + + /** Handle Fees Deductions and reversals of Fees Deductions **/ + else if (savingsTransactionDTO.getTransactionType().isFeeDeduction() && savingsTransactionDTO.isOverdraftTransaction()) { + boolean isPositive = amount.subtract(overdraftAmount).compareTo(BigDecimal.ZERO) > 0; + // Is the Charge a penalty? + if (penaltyPayments.size() > 0) { + this.helper.createAccrualBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, + AccrualAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL, AccrualAccountsForSavings.INCOME_FROM_PENALTIES, + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, overdraftAmount, isReversal, + penaltyPayments); + if (isPositive) { + this.helper.createAccrualBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, + AccrualAccountsForSavings.SAVINGS_CONTROL, AccrualAccountsForSavings.INCOME_FROM_PENALTIES, + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, + amount.subtract(overdraftAmount), isReversal, penaltyPayments); + } + } else { + this.helper.createAccrualBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, + AccrualAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL, AccrualAccountsForSavings.INCOME_FROM_FEES, + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, overdraftAmount, isReversal, + feePayments); + if (isPositive) { + this.helper.createAccrualBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, + AccrualAccountsForSavings.SAVINGS_CONTROL, AccrualAccountsForSavings.INCOME_FROM_FEES, savingsProductId, + paymentTypeId, savingsId, transactionId, transactionDate, amount.subtract(overdraftAmount), isReversal, + feePayments); + } + } + } + + else if (savingsTransactionDTO.getTransactionType().isFeeDeduction()) { + // Is the Charge a penalty? + if (penaltyPayments.size() > 0) { + this.helper.createAccrualBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, + AccrualAccountsForSavings.SAVINGS_CONTROL, AccrualAccountsForSavings.INCOME_FROM_PENALTIES, savingsProductId, + paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal, penaltyPayments); + } else { + this.helper.createAccrualBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, + AccrualAccountsForSavings.SAVINGS_CONTROL, AccrualAccountsForSavings.INCOME_FROM_FEES, savingsProductId, + paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal, feePayments); + } + } + + /** Handle Transfers proposal **/ + else if (savingsTransactionDTO.getTransactionType().isInitiateTransfer()) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), AccrualAccountsForSavings.TRANSFERS_SUSPENSE.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); + } + + /** Handle Transfer Withdrawal or Acceptance **/ + else if (savingsTransactionDTO.getTransactionType().isWithdrawTransfer() + || savingsTransactionDTO.getTransactionType().isApproveTransfer()) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + AccrualAccountsForSavings.TRANSFERS_SUSPENSE.getValue(), AccrualAccountsForSavings.SAVINGS_CONTROL.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); + } + + /** overdraft **/ + else if (savingsTransactionDTO.getTransactionType().isOverdraftInterest()) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + AccrualAccountsForSavings.SAVINGS_REFERENCE.getValue(), AccrualAccountsForSavings.INCOME_FROM_INTEREST.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); + } else if (savingsTransactionDTO.getTransactionType().isWrittenoff()) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + AccrualAccountsForSavings.LOSSES_WRITTEN_OFF.getValue(), + AccrualAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), savingsProductId, paymentTypeId, savingsId, + transactionId, transactionDate, amount, isReversal); + } else if (savingsTransactionDTO.getTransactionType().isOverdraftFee()) { + this.helper.createAccrualBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, + AccrualAccountsForSavings.SAVINGS_REFERENCE, AccrualAccountsForSavings.INCOME_FROM_FEES, savingsProductId, + paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal, feePayments); + } + } + } finally { + AccountingProcessorHelper.clearDepositAccountTypeContext(); + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/CashBasedAccountingProcessorForSavings.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/CashBasedAccountingProcessorForSavings.java index 6b5f7f52e..182e83cb5 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/CashBasedAccountingProcessorForSavings.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/CashBasedAccountingProcessorForSavings.java @@ -1,255 +1,260 @@ -/** - * 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.accounting.journalentry.service; - -import java.math.BigDecimal; -import java.time.LocalDate; -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.apache.fineract.accounting.closure.domain.GLClosure; -import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForSavings; -import org.apache.fineract.accounting.common.AccountingConstants.FinancialActivity; -import org.apache.fineract.accounting.journalentry.data.ChargePaymentDTO; -import org.apache.fineract.accounting.journalentry.data.SavingsDTO; -import org.apache.fineract.accounting.journalentry.data.SavingsTransactionDTO; -import org.apache.fineract.organisation.office.domain.Office; -import org.springframework.stereotype.Component; - -@Component -@RequiredArgsConstructor -public class CashBasedAccountingProcessorForSavings implements AccountingProcessorForSavings { - - private final AccountingProcessorHelper helper; - - @Override - public void createJournalEntriesForSavings(final SavingsDTO savingsDTO) { - final GLClosure latestGLClosure = this.helper.getLatestClosureByBranch(savingsDTO.getOfficeId()); - final Long savingsProductId = savingsDTO.getSavingsProductId(); - final Long savingsId = savingsDTO.getSavingsId(); - final String currencyCode = savingsDTO.getCurrencyCode(); - for (final SavingsTransactionDTO savingsTransactionDTO : savingsDTO.getNewSavingsTransactions()) { - final LocalDate transactionDate = savingsTransactionDTO.getTransactionDate(); - final String transactionId = savingsTransactionDTO.getTransactionId(); - final Office office = this.helper.getOfficeById(savingsTransactionDTO.getOfficeId()); - final Long paymentTypeId = savingsTransactionDTO.getPaymentTypeId(); - final boolean isReversal = savingsTransactionDTO.isReversed(); - final BigDecimal amount = savingsTransactionDTO.getAmount(); - final BigDecimal overdraftAmount = savingsTransactionDTO.getOverdraftAmount(); - final List feePayments = savingsTransactionDTO.getFeePayments(); - final List penaltyPayments = savingsTransactionDTO.getPenaltyPayments(); - - this.helper.checkForBranchClosures(latestGLClosure, transactionDate); - - if (savingsTransactionDTO.getTransactionType().isWithdrawal() && savingsTransactionDTO.isOverdraftTransaction()) { - boolean isPositive = amount.subtract(overdraftAmount).compareTo(BigDecimal.ZERO) > 0; - if (savingsTransactionDTO.isAccountTransfer()) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, - CashAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), FinancialActivity.LIABILITY_TRANSFER.getValue(), - savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, overdraftAmount, isReversal); - if (isPositive) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, - CashAccountsForSavings.SAVINGS_CONTROL.getValue(), FinancialActivity.LIABILITY_TRANSFER.getValue(), - savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, - amount.subtract(overdraftAmount), isReversal); - } - } else { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, - CashAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), - CashAccountsForSavings.SAVINGS_REFERENCE.getValue(), savingsProductId, paymentTypeId, savingsId, transactionId, - transactionDate, overdraftAmount, isReversal); - if (isPositive) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, - CashAccountsForSavings.SAVINGS_CONTROL.getValue(), CashAccountsForSavings.SAVINGS_REFERENCE.getValue(), - savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, - amount.subtract(overdraftAmount), isReversal); - } - } - } else if (savingsTransactionDTO.getTransactionType().isDeposit() && savingsTransactionDTO.isOverdraftTransaction()) { - boolean isPositive = amount.subtract(overdraftAmount).compareTo(BigDecimal.ZERO) > 0; - if (savingsTransactionDTO.isAccountTransfer()) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, - FinancialActivity.LIABILITY_TRANSFER.getValue(), CashAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), - savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, overdraftAmount, isReversal); - if (isPositive) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, - FinancialActivity.LIABILITY_TRANSFER.getValue(), CashAccountsForSavings.SAVINGS_CONTROL.getValue(), - savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, - amount.subtract(overdraftAmount), isReversal); - } - } else { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, - CashAccountsForSavings.SAVINGS_REFERENCE.getValue(), - CashAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), savingsProductId, paymentTypeId, savingsId, - transactionId, transactionDate, overdraftAmount, isReversal); - if (isPositive) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, - CashAccountsForSavings.SAVINGS_REFERENCE.getValue(), CashAccountsForSavings.SAVINGS_CONTROL.getValue(), - savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, - amount.subtract(overdraftAmount), isReversal); - } - } - } - - /** Handle Deposits and reversals of deposits **/ - else if (savingsTransactionDTO.getTransactionType().isDeposit()) { - if (savingsTransactionDTO.isAccountTransfer()) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, - FinancialActivity.LIABILITY_TRANSFER.getValue(), CashAccountsForSavings.SAVINGS_CONTROL.getValue(), - savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); - } else { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, - CashAccountsForSavings.SAVINGS_REFERENCE.getValue(), CashAccountsForSavings.SAVINGS_CONTROL.getValue(), - savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); - } - } - - /** Handle Deposits and reversals of Dividend pay outs **/ - else if (savingsTransactionDTO.getTransactionType().isDividendPayout()) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, - FinancialActivity.PAYABLE_DIVIDENDS.getValue(), CashAccountsForSavings.SAVINGS_CONTROL.getValue(), savingsProductId, - paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); - } - /** Handle withdrawals and reversals of withdrawals **/ - else if (savingsTransactionDTO.getTransactionType().isWithdrawal()) { - if (savingsTransactionDTO.isAccountTransfer()) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, - CashAccountsForSavings.SAVINGS_CONTROL.getValue(), FinancialActivity.LIABILITY_TRANSFER.getValue(), - savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); - } else { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, - CashAccountsForSavings.SAVINGS_CONTROL.getValue(), CashAccountsForSavings.SAVINGS_REFERENCE.getValue(), - savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); - } - } - - else if (savingsTransactionDTO.getTransactionType().isEscheat()) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, - CashAccountsForSavings.SAVINGS_CONTROL.getValue(), CashAccountsForSavings.ESCHEAT_LIABILITY.getValue(), - savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); - } - /** - * Handle Interest Applications and reversals of Interest Applications - **/ - else if (savingsTransactionDTO.getTransactionType().isInterestPosting() && savingsTransactionDTO.isOverdraftTransaction()) { - boolean isPositive = amount.subtract(overdraftAmount).compareTo(BigDecimal.ZERO) > 0; - // Post journal entry if earned interest amount is greater than - // zero - if (savingsTransactionDTO.getAmount().compareTo(BigDecimal.ZERO) > 0) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, - CashAccountsForSavings.INTEREST_ON_SAVINGS.getValue(), - CashAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), savingsProductId, paymentTypeId, savingsId, - transactionId, transactionDate, overdraftAmount, isReversal); - if (isPositive) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, - CashAccountsForSavings.INTEREST_ON_SAVINGS.getValue(), CashAccountsForSavings.SAVINGS_CONTROL.getValue(), - savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, - amount.subtract(overdraftAmount), isReversal); - } - } - } - - else if (savingsTransactionDTO.getTransactionType().isInterestPosting()) { - // Post journal entry if earned interest amount is greater than - // zero - if (savingsTransactionDTO.getAmount().compareTo(BigDecimal.ZERO) > 0) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, - CashAccountsForSavings.INTEREST_ON_SAVINGS.getValue(), CashAccountsForSavings.SAVINGS_CONTROL.getValue(), - savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); - } - } - - else if (savingsTransactionDTO.getTransactionType().isWithholdTax()) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavingsTax(office, currencyCode, - CashAccountsForSavings.SAVINGS_CONTROL, CashAccountsForSavings.SAVINGS_REFERENCE, savingsProductId, paymentTypeId, - savingsId, transactionId, transactionDate, amount, isReversal, savingsTransactionDTO.getTaxPayments()); - } - - /** Handle Fees Deductions and reversals of Fees Deductions **/ - else if (savingsTransactionDTO.getTransactionType().isFeeDeduction() && savingsTransactionDTO.isOverdraftTransaction()) { - boolean isPositive = amount.subtract(overdraftAmount).compareTo(BigDecimal.ZERO) > 0; - // Is the Charge a penalty? - if (penaltyPayments.size() > 0) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, - CashAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL, CashAccountsForSavings.INCOME_FROM_PENALTIES, - savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, overdraftAmount, isReversal, - penaltyPayments); - if (isPositive) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, - CashAccountsForSavings.SAVINGS_CONTROL, CashAccountsForSavings.INCOME_FROM_PENALTIES, savingsProductId, - paymentTypeId, savingsId, transactionId, transactionDate, amount.subtract(overdraftAmount), isReversal, - penaltyPayments); - } - } else { - this.helper.createCashBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, - CashAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL, CashAccountsForSavings.INCOME_FROM_FEES, savingsProductId, - paymentTypeId, savingsId, transactionId, transactionDate, overdraftAmount, isReversal, feePayments); - if (isPositive) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, - CashAccountsForSavings.SAVINGS_CONTROL, CashAccountsForSavings.INCOME_FROM_FEES, savingsProductId, - paymentTypeId, savingsId, transactionId, transactionDate, amount.subtract(overdraftAmount), isReversal, - feePayments); - } - } - } - - else if (savingsTransactionDTO.getTransactionType().isFeeDeduction()) { - // Is the Charge a penalty? - if (penaltyPayments.size() > 0) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, - CashAccountsForSavings.SAVINGS_CONTROL, CashAccountsForSavings.INCOME_FROM_PENALTIES, savingsProductId, - paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal, penaltyPayments); - } else { - this.helper.createCashBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, - CashAccountsForSavings.SAVINGS_CONTROL, CashAccountsForSavings.INCOME_FROM_FEES, savingsProductId, - paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal, feePayments); - } - } - - /** Handle Transfers proposal **/ - else if (savingsTransactionDTO.getTransactionType().isInitiateTransfer()) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, - CashAccountsForSavings.SAVINGS_CONTROL.getValue(), CashAccountsForSavings.TRANSFERS_SUSPENSE.getValue(), - savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); - } - - /** Handle Transfer Withdrawal or Acceptance **/ - else if (savingsTransactionDTO.getTransactionType().isWithdrawTransfer() - || savingsTransactionDTO.getTransactionType().isApproveTransfer()) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, - CashAccountsForSavings.TRANSFERS_SUSPENSE.getValue(), CashAccountsForSavings.SAVINGS_CONTROL.getValue(), - savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); - } - - /** overdraft **/ - else if (savingsTransactionDTO.getTransactionType().isOverdraftInterest()) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, - CashAccountsForSavings.SAVINGS_REFERENCE.getValue(), CashAccountsForSavings.INCOME_FROM_INTEREST.getValue(), - savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); - } else if (savingsTransactionDTO.getTransactionType().isWrittenoff()) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, - CashAccountsForSavings.LOSSES_WRITTEN_OFF.getValue(), CashAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), - savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); - } else if (savingsTransactionDTO.getTransactionType().isOverdraftFee()) { - this.helper.createCashBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, - CashAccountsForSavings.SAVINGS_REFERENCE, CashAccountsForSavings.INCOME_FROM_FEES, savingsProductId, paymentTypeId, - savingsId, transactionId, transactionDate, amount, isReversal, feePayments); - } - } - } -} +/** + * 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.accounting.journalentry.service; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.accounting.closure.domain.GLClosure; +import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForSavings; +import org.apache.fineract.accounting.common.AccountingConstants.FinancialActivity; +import org.apache.fineract.accounting.journalentry.data.ChargePaymentDTO; +import org.apache.fineract.accounting.journalentry.data.SavingsDTO; +import org.apache.fineract.accounting.journalentry.data.SavingsTransactionDTO; +import org.apache.fineract.organisation.office.domain.Office; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class CashBasedAccountingProcessorForSavings implements AccountingProcessorForSavings { + + private final AccountingProcessorHelper helper; + + @Override + public void createJournalEntriesForSavings(final SavingsDTO savingsDTO) { + AccountingProcessorHelper.setDepositAccountTypeContext(savingsDTO.getDepositAccountType()); + try { + final GLClosure latestGLClosure = this.helper.getLatestClosureByBranch(savingsDTO.getOfficeId()); + final Long savingsProductId = savingsDTO.getSavingsProductId(); + final Long savingsId = savingsDTO.getSavingsId(); + final String currencyCode = savingsDTO.getCurrencyCode(); + for (final SavingsTransactionDTO savingsTransactionDTO : savingsDTO.getNewSavingsTransactions()) { + final LocalDate transactionDate = savingsTransactionDTO.getTransactionDate(); + final String transactionId = savingsTransactionDTO.getTransactionId(); + final Office office = this.helper.getOfficeById(savingsTransactionDTO.getOfficeId()); + final Long paymentTypeId = savingsTransactionDTO.getPaymentTypeId(); + final boolean isReversal = savingsTransactionDTO.isReversed(); + final BigDecimal amount = savingsTransactionDTO.getAmount(); + final BigDecimal overdraftAmount = savingsTransactionDTO.getOverdraftAmount(); + final List feePayments = savingsTransactionDTO.getFeePayments(); + final List penaltyPayments = savingsTransactionDTO.getPenaltyPayments(); + + this.helper.checkForBranchClosures(latestGLClosure, transactionDate); + + if (savingsTransactionDTO.getTransactionType().isWithdrawal() && savingsTransactionDTO.isOverdraftTransaction()) { + boolean isPositive = amount.subtract(overdraftAmount).compareTo(BigDecimal.ZERO) > 0; + if (savingsTransactionDTO.isAccountTransfer()) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + CashAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), FinancialActivity.LIABILITY_TRANSFER.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, overdraftAmount, isReversal); + if (isPositive) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + CashAccountsForSavings.SAVINGS_CONTROL.getValue(), FinancialActivity.LIABILITY_TRANSFER.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, + amount.subtract(overdraftAmount), isReversal); + } + } else { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + CashAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), + CashAccountsForSavings.SAVINGS_REFERENCE.getValue(), savingsProductId, paymentTypeId, savingsId, transactionId, + transactionDate, overdraftAmount, isReversal); + if (isPositive) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + CashAccountsForSavings.SAVINGS_CONTROL.getValue(), CashAccountsForSavings.SAVINGS_REFERENCE.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, + amount.subtract(overdraftAmount), isReversal); + } + } + } else if (savingsTransactionDTO.getTransactionType().isDeposit() && savingsTransactionDTO.isOverdraftTransaction()) { + boolean isPositive = amount.subtract(overdraftAmount).compareTo(BigDecimal.ZERO) > 0; + if (savingsTransactionDTO.isAccountTransfer()) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + FinancialActivity.LIABILITY_TRANSFER.getValue(), CashAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, overdraftAmount, isReversal); + if (isPositive) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + FinancialActivity.LIABILITY_TRANSFER.getValue(), CashAccountsForSavings.SAVINGS_CONTROL.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, + amount.subtract(overdraftAmount), isReversal); + } + } else { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + CashAccountsForSavings.SAVINGS_REFERENCE.getValue(), + CashAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), savingsProductId, paymentTypeId, savingsId, + transactionId, transactionDate, overdraftAmount, isReversal); + if (isPositive) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + CashAccountsForSavings.SAVINGS_REFERENCE.getValue(), CashAccountsForSavings.SAVINGS_CONTROL.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, + amount.subtract(overdraftAmount), isReversal); + } + } + } + + /** Handle Deposits and reversals of deposits **/ + else if (savingsTransactionDTO.getTransactionType().isDeposit()) { + if (savingsTransactionDTO.isAccountTransfer()) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + FinancialActivity.LIABILITY_TRANSFER.getValue(), CashAccountsForSavings.SAVINGS_CONTROL.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); + } else { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + CashAccountsForSavings.SAVINGS_REFERENCE.getValue(), CashAccountsForSavings.SAVINGS_CONTROL.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); + } + } + + /** Handle Deposits and reversals of Dividend pay outs **/ + else if (savingsTransactionDTO.getTransactionType().isDividendPayout()) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + FinancialActivity.PAYABLE_DIVIDENDS.getValue(), CashAccountsForSavings.SAVINGS_CONTROL.getValue(), savingsProductId, + paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); + } + /** Handle withdrawals and reversals of withdrawals **/ + else if (savingsTransactionDTO.getTransactionType().isWithdrawal()) { + if (savingsTransactionDTO.isAccountTransfer()) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + CashAccountsForSavings.SAVINGS_CONTROL.getValue(), FinancialActivity.LIABILITY_TRANSFER.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); + } else { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + CashAccountsForSavings.SAVINGS_CONTROL.getValue(), CashAccountsForSavings.SAVINGS_REFERENCE.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); + } + } + + else if (savingsTransactionDTO.getTransactionType().isEscheat()) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + CashAccountsForSavings.SAVINGS_CONTROL.getValue(), CashAccountsForSavings.ESCHEAT_LIABILITY.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); + } + /** + * Handle Interest Applications and reversals of Interest Applications + **/ + else if (savingsTransactionDTO.getTransactionType().isInterestPosting() && savingsTransactionDTO.isOverdraftTransaction()) { + boolean isPositive = amount.subtract(overdraftAmount).compareTo(BigDecimal.ZERO) > 0; + // Post journal entry if earned interest amount is greater than + // zero + if (savingsTransactionDTO.getAmount().compareTo(BigDecimal.ZERO) > 0) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + CashAccountsForSavings.INTEREST_ON_SAVINGS.getValue(), + CashAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), savingsProductId, paymentTypeId, savingsId, + transactionId, transactionDate, overdraftAmount, isReversal); + if (isPositive) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + CashAccountsForSavings.INTEREST_ON_SAVINGS.getValue(), CashAccountsForSavings.SAVINGS_CONTROL.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, + amount.subtract(overdraftAmount), isReversal); + } + } + } + + else if (savingsTransactionDTO.getTransactionType().isInterestPosting()) { + // Post journal entry if earned interest amount is greater than + // zero + if (savingsTransactionDTO.getAmount().compareTo(BigDecimal.ZERO) > 0) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + CashAccountsForSavings.INTEREST_ON_SAVINGS.getValue(), CashAccountsForSavings.SAVINGS_CONTROL.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); + } + } + + else if (savingsTransactionDTO.getTransactionType().isWithholdTax()) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavingsTax(office, currencyCode, + CashAccountsForSavings.SAVINGS_CONTROL, CashAccountsForSavings.SAVINGS_REFERENCE, savingsProductId, paymentTypeId, + savingsId, transactionId, transactionDate, amount, isReversal, savingsTransactionDTO.getTaxPayments()); + } + + /** Handle Fees Deductions and reversals of Fees Deductions **/ + else if (savingsTransactionDTO.getTransactionType().isFeeDeduction() && savingsTransactionDTO.isOverdraftTransaction()) { + boolean isPositive = amount.subtract(overdraftAmount).compareTo(BigDecimal.ZERO) > 0; + // Is the Charge a penalty? + if (penaltyPayments.size() > 0) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, + CashAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL, CashAccountsForSavings.INCOME_FROM_PENALTIES, + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, overdraftAmount, isReversal, + penaltyPayments); + if (isPositive) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, + CashAccountsForSavings.SAVINGS_CONTROL, CashAccountsForSavings.INCOME_FROM_PENALTIES, savingsProductId, + paymentTypeId, savingsId, transactionId, transactionDate, amount.subtract(overdraftAmount), isReversal, + penaltyPayments); + } + } else { + this.helper.createCashBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, + CashAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL, CashAccountsForSavings.INCOME_FROM_FEES, savingsProductId, + paymentTypeId, savingsId, transactionId, transactionDate, overdraftAmount, isReversal, feePayments); + if (isPositive) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, + CashAccountsForSavings.SAVINGS_CONTROL, CashAccountsForSavings.INCOME_FROM_FEES, savingsProductId, + paymentTypeId, savingsId, transactionId, transactionDate, amount.subtract(overdraftAmount), isReversal, + feePayments); + } + } + } + + else if (savingsTransactionDTO.getTransactionType().isFeeDeduction()) { + // Is the Charge a penalty? + if (penaltyPayments.size() > 0) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, + CashAccountsForSavings.SAVINGS_CONTROL, CashAccountsForSavings.INCOME_FROM_PENALTIES, savingsProductId, + paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal, penaltyPayments); + } else { + this.helper.createCashBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, + CashAccountsForSavings.SAVINGS_CONTROL, CashAccountsForSavings.INCOME_FROM_FEES, savingsProductId, + paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal, feePayments); + } + } + + /** Handle Transfers proposal **/ + else if (savingsTransactionDTO.getTransactionType().isInitiateTransfer()) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + CashAccountsForSavings.SAVINGS_CONTROL.getValue(), CashAccountsForSavings.TRANSFERS_SUSPENSE.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); + } + + /** Handle Transfer Withdrawal or Acceptance **/ + else if (savingsTransactionDTO.getTransactionType().isWithdrawTransfer() + || savingsTransactionDTO.getTransactionType().isApproveTransfer()) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + CashAccountsForSavings.TRANSFERS_SUSPENSE.getValue(), CashAccountsForSavings.SAVINGS_CONTROL.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); + } + + /** overdraft **/ + else if (savingsTransactionDTO.getTransactionType().isOverdraftInterest()) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + CashAccountsForSavings.SAVINGS_REFERENCE.getValue(), CashAccountsForSavings.INCOME_FROM_INTEREST.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); + } else if (savingsTransactionDTO.getTransactionType().isWrittenoff()) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode, + CashAccountsForSavings.LOSSES_WRITTEN_OFF.getValue(), CashAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), + savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal); + } else if (savingsTransactionDTO.getTransactionType().isOverdraftFee()) { + this.helper.createCashBasedJournalEntriesAndReversalsForSavingsCharges(office, currencyCode, + CashAccountsForSavings.SAVINGS_REFERENCE, CashAccountsForSavings.INCOME_FROM_FEES, savingsProductId, paymentTypeId, + savingsId, transactionId, transactionDate, amount, isReversal, feePayments); + } + } + } finally { + AccountingProcessorHelper.clearDepositAccountTypeContext(); + } + } +} 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 7299b14ca..519edb532 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 @@ -1,757 +1,765 @@ -/** - * 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.accounting.journalentry.service; - -import static java.util.Objects.requireNonNull; - -import java.math.BigDecimal; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.time.LocalDate; -import java.time.OffsetDateTime; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.StringUtils; -import org.apache.fineract.accounting.common.AccountingEnumerations; -import org.apache.fineract.accounting.financialactivityaccount.domain.FinancialActivityAccount; -import org.apache.fineract.accounting.financialactivityaccount.domain.FinancialActivityAccountRepositoryWrapper; -import org.apache.fineract.accounting.glaccount.data.GLAccountData; -import org.apache.fineract.accounting.glaccount.domain.GLAccountType; -import org.apache.fineract.accounting.glaccount.service.GLAccountReadPlatformService; -import org.apache.fineract.accounting.journalentry.data.DayWiseSummaryData; -import org.apache.fineract.accounting.journalentry.data.GLAccountLedgerReportData; -import org.apache.fineract.accounting.journalentry.data.JournalEntryAssociationParametersData; -import org.apache.fineract.accounting.journalentry.data.JournalEntryData; -import org.apache.fineract.accounting.journalentry.data.OfficeOpeningBalancesData; -import org.apache.fineract.accounting.journalentry.data.TransactionDetailData; -import org.apache.fineract.accounting.journalentry.data.TransactionTypeEnumData; -import org.apache.fineract.accounting.journalentry.exception.JournalEntriesNotFoundException; -import org.apache.fineract.infrastructure.core.data.EnumOptionData; -import org.apache.fineract.infrastructure.core.domain.JdbcSupport; -import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException; -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.monetary.data.CurrencyData; -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.loanaccount.data.LoanTransactionEnumData; -import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations; -import org.apache.fineract.portfolio.note.data.NoteData; -import org.apache.fineract.portfolio.paymentdetail.data.PaymentDetailData; -import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData; -import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionEnumData; -import org.apache.fineract.portfolio.savings.service.SavingsEnumerations; -import org.springframework.dao.EmptyResultDataAccessException; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.RowMapper; -import org.springframework.util.CollectionUtils; - -@RequiredArgsConstructor -public class JournalEntryReadPlatformServiceImpl implements JournalEntryReadPlatformService { - - private final JdbcTemplate jdbcTemplate; - private final GLAccountReadPlatformService glAccountReadPlatformService; - private final OfficeReadPlatformService officeReadPlatformService; - private final ColumnValidator columnValidator; - private final FinancialActivityAccountRepositoryWrapper financialActivityAccountRepositoryWrapper; - - private final PaginationHelper paginationHelper; - private final DatabaseSpecificSQLGenerator sqlGenerator; - - protected static class GLJournalEntryMapper implements RowMapper { - - private final JournalEntryAssociationParametersData associationParametersData; - - protected GLJournalEntryMapper(final JournalEntryAssociationParametersData associationParametersData) { - this.associationParametersData = Objects.requireNonNullElseGet(associationParametersData, - JournalEntryAssociationParametersData::new); - } - - public String schema() { - StringBuilder sb = new StringBuilder(); - sb.append(" journalEntry.id as id, glAccount.classification_enum as classification ,").append("journalEntry.transaction_id,") - .append(" glAccount.name as glAccountName, glAccount.gl_code as glAccountCode,glAccount.id as glAccountId, ") - .append(" journalEntry.office_id as officeId, office.name as officeName, journalEntry.ref_num as referenceNumber, ") - .append(" journalEntry.manual_entry as manualEntry,journalEntry.entry_date as transactionDate, ") - .append(" journalEntry.type_enum as entryType,journalEntry.amount as amount, journalEntry.transaction_id as transactionId,") - .append(" journalEntry.entity_type_enum as entityType, journalEntry.entity_id as entityId, creatingUser.id as createdByUserId, ") - .append(" creatingUser.username as createdByUserName, journalEntry.description as comments, ") - .append(" journalEntry.created_on_utc as submittedOnDate, journalEntry.reversed as reversed, ") - .append(" journalEntry.currency_code as currencyCode, curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, ") - .append(" curr.display_symbol as currencyDisplaySymbol, curr.decimal_places as currencyDigits, curr.currency_multiplesof as inMultiplesOf, ") - .append(" eao.external_id as externalAssetOwner "); - if (associationParametersData.isRunningBalanceRequired()) { - sb.append(" ,journalEntry.is_running_balance_calculated as runningBalanceComputed, ") - .append(" journalEntry.office_running_balance as officeRunningBalance, ") - .append(" journalEntry.organization_running_balance as organizationRunningBalance "); - } - if (associationParametersData.isTransactionDetailsRequired()) { - sb.append(" ,pd.receipt_number as receiptNumber, ").append(" pd.check_number as checkNumber, ") - .append(" pd.account_number as accountNumber, ").append(" pt.value as paymentTypeName, ") - .append(" pd.payment_type_id as paymentTypeId,").append(" pd.bank_number as bankNumber, ") - .append(" pd.routing_code as routingCode, ").append(" note.id as noteId, ") - .append(" note.note as transactionNote, ").append(" lt.transaction_type_enum as loanTransactionType, ") - .append(" st.transaction_type_enum as savingsTransactionType "); - } - sb.append(" from acc_gl_journal_entry as journalEntry ") - .append(" left join acc_gl_account as glAccount on glAccount.id = journalEntry.account_id") - .append(" left join m_office as office on office.id = journalEntry.office_id") - .append(" left join m_appuser as creatingUser on creatingUser.id = journalEntry.created_by ") - .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 "); - 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 ") - .append(" left join m_payment_detail as pd on lt.payment_detail_id = pd.id or st.payment_detail_id = pd.id or journalEntry.payment_details_id = pd.id") - .append(" left join m_payment_type as pt on pt.id = pd.payment_type_id ") - .append(" left join m_note as note on lt.id = note.loan_transaction_id or st.id = note.savings_account_transaction_id "); - } - return sb.toString(); - - } - - @Override - public JournalEntryData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { - - final Long id = rs.getLong("id"); - final Long officeId = rs.getLong("officeId"); - final String officeName = rs.getString("officeName"); - final String glCode = rs.getString("glAccountCode"); - final String glAccountName = rs.getString("glAccountName"); - final Long glAccountId = rs.getLong("glAccountId"); - final int accountTypeId = JdbcSupport.getInteger(rs, "classification"); - final EnumOptionData accountType = AccountingEnumerations.gLAccountType(accountTypeId); - final LocalDate transactionDate = JdbcSupport.getLocalDate(rs, "transactionDate"); - final Boolean manualEntry = rs.getBoolean("manualEntry"); - final BigDecimal amount = rs.getBigDecimal("amount"); - final int entryTypeId = JdbcSupport.getInteger(rs, "entryType"); - final EnumOptionData entryType = AccountingEnumerations.journalEntryType(entryTypeId); - final String transactionId = rs.getString("transactionId"); - final Integer entityTypeId = JdbcSupport.getInteger(rs, "entityType"); - EnumOptionData entityType = null; - if (entityTypeId != null) { - entityType = AccountingEnumerations.portfolioProductType(entityTypeId); - } - - final Long entityId = JdbcSupport.getLong(rs, "entityId"); - final Long createdByUserId = rs.getLong("createdByUserId"); - final OffsetDateTime submittedOnDate = rs.getObject("submittedOnDate", OffsetDateTime.class); - final String createdByUserName = rs.getString("createdByUserName"); - final String comments = rs.getString("comments"); - final Boolean reversed = rs.getBoolean("reversed"); - final String referenceNumber = rs.getString("referenceNumber"); - BigDecimal officeRunningBalance = null; - BigDecimal organizationRunningBalance = null; - Boolean runningBalanceComputed = null; - - 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); - - if (associationParametersData.isRunningBalanceRequired()) { - officeRunningBalance = rs.getBigDecimal("officeRunningBalance"); - organizationRunningBalance = rs.getBigDecimal("organizationRunningBalance"); - runningBalanceComputed = rs.getBoolean("runningBalanceComputed"); - } - TransactionDetailData transactionDetailData = null; - - if (associationParametersData.isTransactionDetailsRequired()) { - PaymentDetailData paymentDetailData = null; - final Long paymentTypeId = JdbcSupport.getLong(rs, "paymentTypeId"); - if (paymentTypeId != null) { - final String typeName = rs.getString("paymentTypeName"); - final PaymentTypeData paymentType = PaymentTypeData.builder().id(paymentTypeId).name(typeName).build(); - final String accountNumber = rs.getString("accountNumber"); - final String checkNumber = rs.getString("checkNumber"); - final String routingCode = rs.getString("routingCode"); - final String receiptNumber = rs.getString("receiptNumber"); - final String bankNumber = rs.getString("bankNumber"); - paymentDetailData = new PaymentDetailData(id, paymentType, accountNumber, checkNumber, routingCode, receiptNumber, - bankNumber); - } - NoteData noteData = null; - final Long noteId = JdbcSupport.getLong(rs, "noteId"); - if (noteId != null) { - final String note = rs.getString("transactionNote"); - noteData = NoteData.builder().id(noteId).note(note).build(); - } - Long transaction = null; - if (entityType != null && transactionId != null) { - String numericPart = transactionId.replaceAll("[^\\d]", ""); - if (!numericPart.isEmpty()) { - transaction = Long.parseLong(numericPart); - } - } - - TransactionTypeEnumData transactionTypeEnumData = null; - - if (entityTypeId != null && PortfolioAccountType.fromInt(entityTypeId).isLoanAccount()) { - final Integer loanTxnTypeId = JdbcSupport.getInteger(rs, "loanTransactionType"); - if (loanTxnTypeId != null) { - final LoanTransactionEnumData loanTransactionType = LoanEnumerations.transactionType(loanTxnTypeId); - transactionTypeEnumData = new TransactionTypeEnumData(loanTransactionType.getId(), loanTransactionType.getCode(), - loanTransactionType.getValue()); - } - } else if (entityTypeId != null && PortfolioAccountType.fromInt(entityTypeId).isSavingsAccount()) { - final Integer savingsTxnTypeId = JdbcSupport.getInteger(rs, "savingsTransactionType"); - if (savingsTxnTypeId != null) { - final SavingsAccountTransactionEnumData savingsTransactionType = SavingsEnumerations.transactionType(savingsTxnTypeId); - transactionTypeEnumData = new TransactionTypeEnumData(savingsTransactionType.getId(), savingsTransactionType.getCode(), - savingsTransactionType.getValue()); - } - } - - transactionDetailData = new TransactionDetailData(transaction, paymentDetailData, noteData, transactionTypeEnumData); - } - final String externalAssetOwner = rs.getString("externalAssetOwner"); - - return new JournalEntryData(id, officeId, officeName, glAccountName, glAccountId, glCode, accountType, transactionDate, - entryType, amount, transactionId, manualEntry, entityType, entityId, createdByUserId, submittedOnDate, - createdByUserName, comments, reversed, referenceNumber, officeRunningBalance, organizationRunningBalance, - runningBalanceComputed, transactionDetailData, currency, externalAssetOwner); - } - } - - @Override - public Page retrieveAll(final SearchParameters searchParameters, final Long glAccountId, - final Boolean onlyManualEntries, final LocalDate fromDate, final LocalDate toDate, final LocalDate submittedOnDateFrom, - final LocalDate submittedOnDateTo, final String transactionId, final Integer entityType, - final JournalEntryAssociationParametersData associationParametersData) { - GLJournalEntryMapper rm = getGlJournalEntryMapper(associationParametersData); - final StringBuilder sqlBuilder = new StringBuilder(200); - sqlBuilder.append("select ").append(sqlGenerator.calcFoundRows()).append(" "); - sqlBuilder.append(rm.schema()); - - final Object[] objectArray = new Object[15]; - int arrayPos = 0; - String whereClose = " where "; - - if (StringUtils.isNotBlank(transactionId)) { - sqlBuilder.append(whereClose).append(" journalEntry.transaction_id = ?"); - objectArray[arrayPos] = transactionId; - arrayPos = arrayPos + 1; - - whereClose = " and "; - } - - if (entityType != null && entityType != 0 && (onlyManualEntries == null)) { - - sqlBuilder.append(whereClose).append(" journalEntry.entity_type_enum = ?"); - - objectArray[arrayPos] = entityType; - arrayPos = arrayPos + 1; - - whereClose = " and "; - } - - if (searchParameters.hasOfficeId()) { - sqlBuilder.append(whereClose).append(" journalEntry.office_id = ?"); - objectArray[arrayPos] = searchParameters.getOfficeId(); - arrayPos = arrayPos + 1; - - whereClose = " and "; - } - - if (searchParameters.hasCurrencyCode()) { - sqlBuilder.append(whereClose).append(" journalEntry.currency_code = ?"); - objectArray[arrayPos] = searchParameters.getCurrencyCode(); - arrayPos = arrayPos + 1; - - whereClose = " and "; - } - - if (glAccountId != null && glAccountId != 0) { - sqlBuilder.append(whereClose).append(" journalEntry.account_id = ?"); - objectArray[arrayPos] = glAccountId; - arrayPos = arrayPos + 1; - - whereClose = " and "; - } - - if (fromDate != null || toDate != null) { - if (fromDate != null && toDate != null) { - sqlBuilder.append(whereClose).append(" journalEntry.entry_date between ? and ? "); - whereClose = " and "; - objectArray[arrayPos] = fromDate; - arrayPos = arrayPos + 1; - objectArray[arrayPos] = toDate; - arrayPos = arrayPos + 1; - } else if (fromDate != null) { - sqlBuilder.append(whereClose).append(" journalEntry.entry_date >= ? "); - whereClose = " and "; - objectArray[arrayPos] = fromDate; - arrayPos = arrayPos + 1; - } else { - sqlBuilder.append(whereClose).append(" journalEntry.entry_date <= ? "); - whereClose = " and "; - objectArray[arrayPos] = toDate; - arrayPos = arrayPos + 1; - } - } - - if (submittedOnDateFrom != null || submittedOnDateTo != null) { - if (submittedOnDateFrom != null && submittedOnDateTo != null) { - sqlBuilder.append(whereClose).append(" journalEntry.submitted_on_date between ? and ? "); - whereClose = " and "; - objectArray[arrayPos] = submittedOnDateFrom; - arrayPos = arrayPos + 1; - objectArray[arrayPos] = submittedOnDateTo; - arrayPos = arrayPos + 1; - } else if (fromDate != null) { - sqlBuilder.append(whereClose).append(" journalEntry.submitted_on_date >= ? "); - whereClose = " and "; - objectArray[arrayPos] = submittedOnDateFrom; - arrayPos = arrayPos + 1; - } else { - sqlBuilder.append(whereClose).append(" journalEntry.submitted_on_date <= ? "); - whereClose = " and "; - objectArray[arrayPos] = submittedOnDateTo; - arrayPos = arrayPos + 1; - } - } - - if (onlyManualEntries != null) { - if (onlyManualEntries) { - sqlBuilder.append(whereClose).append(" journalEntry.manual_entry = ?"); - whereClose = " and "; - objectArray[arrayPos] = Boolean.TRUE; - arrayPos = arrayPos + 1; - } - } - - if (searchParameters.hasLoanId()) { - sqlBuilder.append(whereClose) - .append(" journalEntry.loan_transaction_id in (select id from m_loan_transaction where loan_id = ?)"); - objectArray[arrayPos] = searchParameters.getLoanId(); - arrayPos = arrayPos + 1; - - whereClose = " and "; - } - if (searchParameters.hasSavingsId()) { - sqlBuilder.append(whereClose).append( - " journalEntry.savings_transaction_id in (select id from m_savings_account_transaction where savings_account_id = ?)"); - objectArray[arrayPos] = searchParameters.getSavingsId(); - arrayPos = arrayPos + 1; - } - - 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.getOrderBy()); - } - } else { - sqlBuilder.append(" order by journalEntry.entry_date, journalEntry.id"); - } - - 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 = Arrays.copyOf(objectArray, arrayPos); - return this.paginationHelper.fetchPage(this.jdbcTemplate, sqlBuilder.toString(), finalObjectArray, rm); - } - - protected GLJournalEntryMapper getGlJournalEntryMapper(JournalEntryAssociationParametersData associationParametersData) { - return new GLJournalEntryMapper(associationParametersData); - } - - @Override - public JournalEntryData retrieveGLJournalEntryById(final long glJournalEntryId, - JournalEntryAssociationParametersData associationParametersData) { - try { - - final GLJournalEntryMapper rm = getGlJournalEntryMapper(associationParametersData); - // Programmatic query, disable sonar issue - final String sql = "select " + rm.schema() + " where journalEntry.id = ?"; - - return this.jdbcTemplate.queryForObject(sql, rm, glJournalEntryId); // NOSONAR - } catch (final EmptyResultDataAccessException e) { - throw new JournalEntriesNotFoundException(glJournalEntryId, e); - } - } - - @Override - public OfficeOpeningBalancesData retrieveOfficeOpeningBalances(final Long officeId, String currencyCode) { - - final FinancialActivityAccount financialActivityAccountId = this.financialActivityAccountRepositoryWrapper - .findByFinancialActivityTypeWithNotFoundDetection(300); - final Long contraId = financialActivityAccountId.getGlAccount().getId(); - if (contraId == null) { - throw new GeneralPlatformDomainRuleException( - "error.msg.financial.activity.mapping.opening.balance.contra.account.cannot.be.null", - "office-opening-balances-contra-account value can not be null", "office-opening-balances-contra-account"); - } - - final JournalEntryAssociationParametersData associationParametersData = new JournalEntryAssociationParametersData(); - final GLAccountData contraAccount = this.glAccountReadPlatformService.retrieveGLAccountById(contraId, associationParametersData); - if (!GLAccountType.fromInt(requireNonNull(requireNonNull(contraAccount.getType()).getId()).intValue()).isEquityType()) { - throw new GeneralPlatformDomainRuleException( - "error.msg.configuration.opening.balance.contra.account.value.is.invalid.account.type", - "Global configuration 'office-opening-balances-contra-account' value is not an equity type account", contraId); - } - - final OfficeData officeData = this.officeReadPlatformService.retrieveOffice(officeId); - final List allOpeningTransactions = populateAllTransactionsFromGLAccounts(contraId); - final String contraTransactionId = retrieveContraAccountTransactionId(officeId, contraId, currencyCode); - - List existingOpeningBalanceTransactions = new ArrayList<>(); - if (StringUtils.isNotBlank(contraTransactionId)) { - existingOpeningBalanceTransactions = retrieveOfficeBalanceTransactions(officeId, contraTransactionId, currencyCode); - } - final List transactions = populateOpeningBalances(existingOpeningBalanceTransactions, allOpeningTransactions); - final List assetAccountOpeningBalances = new ArrayList<>(); - final List liabilityAccountOpeningBalances = new ArrayList<>(); - final List incomeAccountOpeningBalances = new ArrayList<>(); - final List equityAccountOpeningBalances = new ArrayList<>(); - final List expenseAccountOpeningBalances = new ArrayList<>(); - - for (final JournalEntryData journalEntryData : transactions) { - final GLAccountType type = GLAccountType.fromInt(journalEntryData.getGlAccountType().getId().intValue()); - if (type.isAssetType()) { - assetAccountOpeningBalances.add(journalEntryData); - } else if (type.isLiabilityType()) { - liabilityAccountOpeningBalances.add(journalEntryData); - } else if (type.isEquityType()) { - equityAccountOpeningBalances.add(journalEntryData); - } else if (type.isIncomeType()) { - incomeAccountOpeningBalances.add(journalEntryData); - } else if (type.isExpenseType()) { - expenseAccountOpeningBalances.add(journalEntryData); - } - } - - final LocalDate transactionDate = DateUtils.getBusinessLocalDate(); - - return OfficeOpeningBalancesData.createNew(officeId, officeData.getName(), transactionDate, contraAccount, - assetAccountOpeningBalances, liabilityAccountOpeningBalances, incomeAccountOpeningBalances, equityAccountOpeningBalances, - expenseAccountOpeningBalances); - } - - private List populateOpeningBalances(final List existingOpeningBalanceTransactions, - final List allOpeningTransactions) { - final List allOpeningBalanceTransactions = new ArrayList<>(allOpeningTransactions.size()); - for (final JournalEntryData newOpeningBalanceTransaction : allOpeningTransactions) { - boolean isNewTransactionAddedToCollection = false; - for (final JournalEntryData existingOpeningBalanceTransaction : existingOpeningBalanceTransactions) { - if (newOpeningBalanceTransaction.getGlAccountId().equals(existingOpeningBalanceTransaction.getGlAccountId())) { - allOpeningBalanceTransactions.add(existingOpeningBalanceTransaction); - isNewTransactionAddedToCollection = true; - break; - } - } - if (!isNewTransactionAddedToCollection) { - allOpeningBalanceTransactions.add(newOpeningBalanceTransaction); - } - } - return allOpeningBalanceTransactions; - } - - private List populateAllTransactionsFromGLAccounts(final Long contraId) { - final List glAccounts = this.glAccountReadPlatformService.retrieveAllEnabledDetailGLAccounts(); - final List openingBalanceTransactions = new ArrayList<>(glAccounts.size()); - - for (final GLAccountData glAccountData : glAccounts) { - if (!contraId.equals(glAccountData.getId())) { - final JournalEntryData openingBalanceTransaction = JournalEntryData.fromGLAccountData(glAccountData); - openingBalanceTransactions.add(openingBalanceTransaction); - } - } - return openingBalanceTransactions; - } - - private List retrieveOfficeBalanceTransactions(final Long officeId, final String transactionId, - final String currencyCode) { - final Long contraId = null; - return retrieveContraTransactions(officeId, contraId, transactionId, currencyCode).getPageItems(); - } - - private String retrieveContraAccountTransactionId(final Long officeId, final Long contraId, final String currencyCode) { - final String transactionId = ""; - final Page contraJournalEntries = retrieveContraTransactions(officeId, contraId, transactionId, currencyCode); - if (!CollectionUtils.isEmpty(contraJournalEntries.getPageItems())) { - final JournalEntryData contraTransaction = contraJournalEntries.getPageItems() - .get(contraJournalEntries.getPageItems().size() - 1); - return contraTransaction.getTransactionId(); - } - return transactionId; - } - - private Page retrieveContraTransactions(final Long officeId, final Long contraId, final String transactionId, - final String currencyCode) { - final Integer entityType = null; - final Boolean onlyManualEntries = null; - final LocalDate fromDate = null; - final LocalDate toDate = null; - final LocalDate submittedOnDateFrom = null; - final LocalDate submittedOnDateTo = null; - final JournalEntryAssociationParametersData associationParametersData = null; - - final SearchParameters searchParameters = SearchParameters.builder().orphansOnly(false).officeId(officeId).offset(0) - .orderBy("journalEntry.id").sortOrder("ASC").currencyCode(currencyCode).build(); - - return retrieveAll(searchParameters, contraId, onlyManualEntries, fromDate, toDate, submittedOnDateFrom, submittedOnDateTo, - transactionId, entityType, associationParametersData); - - } - - @Override - public Page retrieveJournalEntriesByEntityId(String transactionId, Long entityId, Integer entityType) { - JournalEntryAssociationParametersData associationParametersData = new JournalEntryAssociationParametersData(true, true); - try { - final GLJournalEntryMapper rm = getGlJournalEntryMapper(associationParametersData); - final String sql = "select " + rm.schema() - + " where journalEntry.transaction_id = ? and journalEntry.entity_id = ? and journalEntry.entity_type_enum = ?"; - Object[] data = { transactionId, entityId, entityType }; - return this.paginationHelper.fetchPage(this.jdbcTemplate, sql, data, rm); - } catch (final EmptyResultDataAccessException e) { - throw new JournalEntriesNotFoundException(entityId, e); - } - } - - @Override - public GLAccountLedgerReportData retrieveGLAccountLedger(final Long glAccountId, final Long officeId, - final Boolean onlyManualEntries, final LocalDate fromDate, final LocalDate toDate, - final String transactionId, final SearchParameters searchParameters) { - - // ── 1. Fetch GL Account details ────────────────────────────────────── - GLAccountData glAccountData = null; - boolean isDebitNormal = true; // ASSET / EXPENSE = debit-normal - if (glAccountId != null && glAccountId > 0) { - glAccountData = this.glAccountReadPlatformService.retrieveGLAccountById(glAccountId, - new JournalEntryAssociationParametersData()); - if (glAccountData != null && glAccountData.getType() != null) { - final int typeId = glAccountData.getType().getId().intValue(); - // 1=ASSET, 2=LIABILITY, 3=EQUITY, 4=INCOME, 5=EXPENSE - isDebitNormal = (typeId == 1 || typeId == 5); - } - } - - // ── 2. Compute opening balance (all entries before fromDate) ───────── - BigDecimal openingBalance = BigDecimal.ZERO; - if (glAccountId != null && glAccountId > 0 && fromDate != null) { - final List obParams = new ArrayList<>(); - final StringBuilder obSql = new StringBuilder( - "SELECT COALESCE(SUM(CASE WHEN je.type_enum = 2 THEN je.amount ELSE 0 END), 0) AS ob_debit, " - + "COALESCE(SUM(CASE WHEN je.type_enum = 1 THEN je.amount ELSE 0 END), 0) AS ob_credit " - + "FROM acc_gl_journal_entry je " - + "WHERE je.account_id = ? AND je.entry_date < ?"); - obParams.add(glAccountId); - obParams.add(fromDate); - if (officeId != null && officeId > 0) { - obSql.append(" AND je.office_id = ?"); - obParams.add(officeId); - } - final BigDecimal[] ob = this.jdbcTemplate.queryForObject(obSql.toString(), - (rs, rowNum) -> new BigDecimal[]{ - Objects.requireNonNullElse(rs.getBigDecimal("ob_debit"), BigDecimal.ZERO), - Objects.requireNonNullElse(rs.getBigDecimal("ob_credit"), BigDecimal.ZERO) - }, obParams.toArray()); - if (ob != null) { - openingBalance = isDebitNormal ? ob[0].subtract(ob[1]) : ob[1].subtract(ob[0]); - } - } - - // ── 3. Day-wise summary for the period ─────────────────────────────── - final List dayWiseSummary = new ArrayList<>(); - { - final List dwParams = new ArrayList<>(); - final StringBuilder dwSql = new StringBuilder( - "SELECT je.entry_date AS dw_date, " - + "COALESCE(SUM(CASE WHEN je.type_enum = 2 THEN je.amount ELSE 0 END), 0) AS dw_debit, " - + "COALESCE(SUM(CASE WHEN je.type_enum = 1 THEN je.amount ELSE 0 END), 0) AS dw_credit, " - + "COUNT(*) AS dw_count " - + "FROM acc_gl_journal_entry je " - + "WHERE 1=1"); - if (glAccountId != null && glAccountId > 0) { - dwSql.append(" AND je.account_id = ?"); - dwParams.add(glAccountId); - } - if (officeId != null && officeId > 0) { - dwSql.append(" AND je.office_id = ?"); - dwParams.add(officeId); - } - if (fromDate != null) { - dwSql.append(" AND je.entry_date >= ?"); - dwParams.add(fromDate); - } - if (toDate != null) { - dwSql.append(" AND je.entry_date <= ?"); - dwParams.add(toDate); - } - if (onlyManualEntries != null && onlyManualEntries) { - dwSql.append(" AND je.manual_entry = true"); - } - dwSql.append(" GROUP BY je.entry_date ORDER BY je.entry_date ASC"); - - final boolean debitNormalFinal = isDebitNormal; - final List> rows = this.jdbcTemplate.queryForList(dwSql.toString(), dwParams.toArray()); - - BigDecimal running = openingBalance; - for (final Map row : rows) { - // Handle both java.sql.Date (older drivers) and java.time.LocalDate (newer drivers) - final Object rawDate = row.get("dw_date"); - LocalDate date; - if (rawDate instanceof LocalDate) { - date = (LocalDate) rawDate; - } else if (rawDate instanceof java.sql.Date) { - date = ((java.sql.Date) rawDate).toLocalDate(); - } else { - date = LocalDate.parse(String.valueOf(rawDate).substring(0, 10)); - } - final BigDecimal dr = Objects.requireNonNullElse((BigDecimal) row.get("dw_debit"), BigDecimal.ZERO); - final BigDecimal cr = Objects.requireNonNullElse((BigDecimal) row.get("dw_credit"), BigDecimal.ZERO); - final int count = ((Number) row.get("dw_count")).intValue(); - final BigDecimal net = debitNormalFinal ? dr.subtract(cr) : cr.subtract(dr); - final BigDecimal closing = running.add(net); - dayWiseSummary.add(DayWiseSummaryData.builder() - .date(date) - .openingBalance(running) - .totalDebit(dr) - .totalCredit(cr) - .netMovement(net) - .closingBalance(closing) - .entryCount(count) - .build()); - running = closing; - } - } - - // ── 4. Paginated journal entries — only fetch if limit > 0 ─────────── - final JournalEntryAssociationParametersData assocParams = new JournalEntryAssociationParametersData(true, true); - final int reqLimit = searchParameters != null && searchParameters.getLimit() != null - ? searchParameters.getLimit() : 0; - final Page entries; - if (reqLimit > 0) { - entries = retrieveAll(searchParameters, glAccountId, onlyManualEntries, - fromDate, toDate, null, null, transactionId, null, assocParams); - } else { - // Summary-only request — return empty page, no SQL needed - entries = new Page<>(new ArrayList<>(), 0); - } - - // ── 5. Compute period totals ───────────────────────────────────────── - BigDecimal totalDebit = BigDecimal.ZERO; - BigDecimal totalCredit = BigDecimal.ZERO; - { - final List totParams = new ArrayList<>(); - final StringBuilder totSql = new StringBuilder( - "SELECT COALESCE(SUM(CASE WHEN je.type_enum = 2 THEN je.amount ELSE 0 END), 0) AS totalDebit, " - + "COALESCE(SUM(CASE WHEN je.type_enum = 1 THEN je.amount ELSE 0 END), 0) AS totalCredit " - + "FROM acc_gl_journal_entry je WHERE 1=1"); - if (glAccountId != null && glAccountId > 0) { - totSql.append(" AND je.account_id = ?"); - totParams.add(glAccountId); - } - if (officeId != null && officeId > 0) { - totSql.append(" AND je.office_id = ?"); - totParams.add(officeId); - } - if (fromDate != null) { - totSql.append(" AND je.entry_date >= ?"); - totParams.add(fromDate); - } - if (toDate != null) { - totSql.append(" AND je.entry_date <= ?"); - totParams.add(toDate); - } - if (onlyManualEntries != null && onlyManualEntries) { - totSql.append(" AND je.manual_entry = true"); - } - final BigDecimal[] totResult = this.jdbcTemplate.queryForObject(totSql.toString(), - (rs, rowNum) -> new BigDecimal[]{ - Objects.requireNonNullElse(rs.getBigDecimal("totalDebit"), BigDecimal.ZERO), - Objects.requireNonNullElse(rs.getBigDecimal("totalCredit"), BigDecimal.ZERO) - }, totParams.toArray()); - if (totResult != null) { - totalDebit = totResult[0]; - totalCredit = totResult[1]; - } - } - - final BigDecimal netMovement = isDebitNormal ? totalDebit.subtract(totalCredit) : totalCredit.subtract(totalDebit); - final BigDecimal closingBalance = openingBalance.add(netMovement); - - // ── 6. Total entries count ──────────────────────────────────────────── - long totalEntries = 0L; - { - final List cntParams = new ArrayList<>(); - final StringBuilder cntSql = new StringBuilder( - "SELECT COUNT(*) FROM acc_gl_journal_entry je WHERE 1=1"); - if (glAccountId != null && glAccountId > 0) { - cntSql.append(" AND je.account_id = ?"); - cntParams.add(glAccountId); - } - if (officeId != null && officeId > 0) { - cntSql.append(" AND je.office_id = ?"); - cntParams.add(officeId); - } - if (fromDate != null) { - cntSql.append(" AND je.entry_date >= ?"); - cntParams.add(fromDate); - } - if (toDate != null) { - cntSql.append(" AND je.entry_date <= ?"); - cntParams.add(toDate); - } - if (onlyManualEntries != null && onlyManualEntries) { - cntSql.append(" AND je.manual_entry = true"); - } - final Long cnt = this.jdbcTemplate.queryForObject(cntSql.toString(), Long.class, cntParams.toArray()); - if (cnt != null) { totalEntries = cnt; } - } - - return GLAccountLedgerReportData.builder() - .glAccount(glAccountData) - .openingBalance(openingBalance) - .closingBalance(closingBalance) - .totalDebit(totalDebit) - .totalCredit(totalCredit) - .netMovement(netMovement) - .debitNormal(isDebitNormal) - .fromDate(fromDate) - .toDate(toDate) - .dayWiseSummary(dayWiseSummary) - .totalEntries(totalEntries) - .entries(entries) - .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.accounting.journalentry.service; + +import static java.util.Objects.requireNonNull; + +import java.math.BigDecimal; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.LocalDate; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.accounting.common.AccountingEnumerations; +import org.apache.fineract.accounting.financialactivityaccount.domain.FinancialActivityAccount; +import org.apache.fineract.accounting.financialactivityaccount.domain.FinancialActivityAccountRepositoryWrapper; +import org.apache.fineract.accounting.glaccount.data.GLAccountData; +import org.apache.fineract.accounting.glaccount.domain.GLAccountType; +import org.apache.fineract.accounting.glaccount.service.GLAccountReadPlatformService; +import org.apache.fineract.accounting.journalentry.data.DayWiseSummaryData; +import org.apache.fineract.accounting.journalentry.data.GLAccountLedgerReportData; +import org.apache.fineract.accounting.journalentry.data.JournalEntryAssociationParametersData; +import org.apache.fineract.accounting.journalentry.data.JournalEntryData; +import org.apache.fineract.accounting.journalentry.data.OfficeOpeningBalancesData; +import org.apache.fineract.accounting.journalentry.data.TransactionDetailData; +import org.apache.fineract.accounting.journalentry.data.TransactionTypeEnumData; +import org.apache.fineract.accounting.journalentry.exception.JournalEntriesNotFoundException; +import org.apache.fineract.infrastructure.core.data.EnumOptionData; +import org.apache.fineract.infrastructure.core.domain.JdbcSupport; +import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException; +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.monetary.data.CurrencyData; +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.loanaccount.data.LoanTransactionEnumData; +import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations; +import org.apache.fineract.portfolio.note.data.NoteData; +import org.apache.fineract.portfolio.paymentdetail.data.PaymentDetailData; +import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData; +import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionEnumData; +import org.apache.fineract.portfolio.savings.service.SavingsEnumerations; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.util.CollectionUtils; + +@RequiredArgsConstructor +public class JournalEntryReadPlatformServiceImpl implements JournalEntryReadPlatformService { + + private final JdbcTemplate jdbcTemplate; + private final GLAccountReadPlatformService glAccountReadPlatformService; + private final OfficeReadPlatformService officeReadPlatformService; + private final ColumnValidator columnValidator; + private final FinancialActivityAccountRepositoryWrapper financialActivityAccountRepositoryWrapper; + + private final PaginationHelper paginationHelper; + private final DatabaseSpecificSQLGenerator sqlGenerator; + + protected static class GLJournalEntryMapper implements RowMapper { + + private final JournalEntryAssociationParametersData associationParametersData; + + protected GLJournalEntryMapper(final JournalEntryAssociationParametersData associationParametersData) { + this.associationParametersData = Objects.requireNonNullElseGet(associationParametersData, + JournalEntryAssociationParametersData::new); + } + + public String schema() { + StringBuilder sb = new StringBuilder(); + sb.append(" journalEntry.id as id, glAccount.classification_enum as classification ,").append("journalEntry.transaction_id,") + .append(" glAccount.name as glAccountName, glAccount.gl_code as glAccountCode,glAccount.id as glAccountId, ") + .append(" journalEntry.office_id as officeId, office.name as officeName, journalEntry.ref_num as referenceNumber, ") + .append(" journalEntry.manual_entry as manualEntry,journalEntry.entry_date as transactionDate, ") + .append(" journalEntry.type_enum as entryType,journalEntry.amount as amount, journalEntry.transaction_id as transactionId,") + .append(" journalEntry.entity_type_enum as entityType, journalEntry.entity_id as entityId, creatingUser.id as createdByUserId, ") + .append(" creatingUser.username as createdByUserName, journalEntry.description as comments, ") + .append(" journalEntry.created_on_utc as submittedOnDate, journalEntry.reversed as reversed, ") + .append(" journalEntry.currency_code as currencyCode, curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, ") + .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(" ELSE NULL ") + .append(" END as savingsAccountNo "); + if (associationParametersData.isRunningBalanceRequired()) { + sb.append(" ,journalEntry.is_running_balance_calculated as runningBalanceComputed, ") + .append(" journalEntry.office_running_balance as officeRunningBalance, ") + .append(" journalEntry.organization_running_balance as organizationRunningBalance "); + } + if (associationParametersData.isTransactionDetailsRequired()) { + sb.append(" ,pd.receipt_number as receiptNumber, ").append(" pd.check_number as checkNumber, ") + .append(" pd.account_number as accountNumber, ").append(" pt.value as paymentTypeName, ") + .append(" pd.payment_type_id as paymentTypeId,").append(" pd.bank_number as bankNumber, ") + .append(" pd.routing_code as routingCode, ").append(" note.id as noteId, ") + .append(" note.note as transactionNote, ").append(" lt.transaction_type_enum as loanTransactionType, ") + .append(" st.transaction_type_enum as savingsTransactionType "); + } + sb.append(" from acc_gl_journal_entry as journalEntry ") + .append(" left join acc_gl_account as glAccount on glAccount.id = journalEntry.account_id") + .append(" left join m_office as office on office.id = journalEntry.office_id") + .append(" left join m_appuser as creatingUser on creatingUser.id = journalEntry.created_by ") + .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%' "); + 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 ") + .append(" left join m_payment_detail as pd on lt.payment_detail_id = pd.id or st.payment_detail_id = pd.id or journalEntry.payment_details_id = pd.id") + .append(" left join m_payment_type as pt on pt.id = pd.payment_type_id ") + .append(" left join m_note as note on lt.id = note.loan_transaction_id or st.id = note.savings_account_transaction_id "); + } + return sb.toString(); + + } + + @Override + public JournalEntryData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { + + final Long id = rs.getLong("id"); + final Long officeId = rs.getLong("officeId"); + final String officeName = rs.getString("officeName"); + final String glCode = rs.getString("glAccountCode"); + final String glAccountName = rs.getString("glAccountName"); + final Long glAccountId = rs.getLong("glAccountId"); + final int accountTypeId = JdbcSupport.getInteger(rs, "classification"); + final EnumOptionData accountType = AccountingEnumerations.gLAccountType(accountTypeId); + final LocalDate transactionDate = JdbcSupport.getLocalDate(rs, "transactionDate"); + final Boolean manualEntry = rs.getBoolean("manualEntry"); + final BigDecimal amount = rs.getBigDecimal("amount"); + final int entryTypeId = JdbcSupport.getInteger(rs, "entryType"); + final EnumOptionData entryType = AccountingEnumerations.journalEntryType(entryTypeId); + final String transactionId = rs.getString("transactionId"); + final Integer entityTypeId = JdbcSupport.getInteger(rs, "entityType"); + EnumOptionData entityType = null; + if (entityTypeId != null) { + entityType = AccountingEnumerations.portfolioProductType(entityTypeId); + } + + final Long entityId = JdbcSupport.getLong(rs, "entityId"); + final Long createdByUserId = rs.getLong("createdByUserId"); + final OffsetDateTime submittedOnDate = rs.getObject("submittedOnDate", OffsetDateTime.class); + final String createdByUserName = rs.getString("createdByUserName"); + final String comments = rs.getString("comments"); + final Boolean reversed = rs.getBoolean("reversed"); + final String referenceNumber = rs.getString("referenceNumber"); + BigDecimal officeRunningBalance = null; + BigDecimal organizationRunningBalance = null; + Boolean runningBalanceComputed = null; + + 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); + + if (associationParametersData.isRunningBalanceRequired()) { + officeRunningBalance = rs.getBigDecimal("officeRunningBalance"); + organizationRunningBalance = rs.getBigDecimal("organizationRunningBalance"); + runningBalanceComputed = rs.getBoolean("runningBalanceComputed"); + } + TransactionDetailData transactionDetailData = null; + + if (associationParametersData.isTransactionDetailsRequired()) { + PaymentDetailData paymentDetailData = null; + final Long paymentTypeId = JdbcSupport.getLong(rs, "paymentTypeId"); + if (paymentTypeId != null) { + final String typeName = rs.getString("paymentTypeName"); + final PaymentTypeData paymentType = PaymentTypeData.builder().id(paymentTypeId).name(typeName).build(); + final String accountNumber = rs.getString("accountNumber"); + final String checkNumber = rs.getString("checkNumber"); + final String routingCode = rs.getString("routingCode"); + final String receiptNumber = rs.getString("receiptNumber"); + final String bankNumber = rs.getString("bankNumber"); + paymentDetailData = new PaymentDetailData(id, paymentType, accountNumber, checkNumber, routingCode, receiptNumber, + bankNumber); + } + NoteData noteData = null; + final Long noteId = JdbcSupport.getLong(rs, "noteId"); + if (noteId != null) { + final String note = rs.getString("transactionNote"); + noteData = NoteData.builder().id(noteId).note(note).build(); + } + Long transaction = null; + if (entityType != null && transactionId != null) { + String numericPart = transactionId.replaceAll("[^\\d]", ""); + if (!numericPart.isEmpty()) { + transaction = Long.parseLong(numericPart); + } + } + + TransactionTypeEnumData transactionTypeEnumData = null; + + if (entityTypeId != null && PortfolioAccountType.fromInt(entityTypeId).isLoanAccount()) { + final Integer loanTxnTypeId = JdbcSupport.getInteger(rs, "loanTransactionType"); + if (loanTxnTypeId != null) { + final LoanTransactionEnumData loanTransactionType = LoanEnumerations.transactionType(loanTxnTypeId); + transactionTypeEnumData = new TransactionTypeEnumData(loanTransactionType.getId(), loanTransactionType.getCode(), + loanTransactionType.getValue()); + } + } else if (entityTypeId != null && PortfolioAccountType.fromInt(entityTypeId).isSavingsAccount()) { + final Integer savingsTxnTypeId = JdbcSupport.getInteger(rs, "savingsTransactionType"); + if (savingsTxnTypeId != null) { + final SavingsAccountTransactionEnumData savingsTransactionType = SavingsEnumerations.transactionType(savingsTxnTypeId); + transactionTypeEnumData = new TransactionTypeEnumData(savingsTransactionType.getId(), savingsTransactionType.getCode(), + savingsTransactionType.getValue()); + } + } + + transactionDetailData = new TransactionDetailData(transaction, paymentDetailData, noteData, transactionTypeEnumData); + } + final String externalAssetOwner = rs.getString("externalAssetOwner"); + final String savingsAccountNo = rs.getString("savingsAccountNo"); + + return new JournalEntryData(id, officeId, officeName, glAccountName, glAccountId, glCode, accountType, transactionDate, + entryType, amount, transactionId, manualEntry, entityType, entityId, createdByUserId, submittedOnDate, + createdByUserName, comments, reversed, referenceNumber, officeRunningBalance, organizationRunningBalance, + runningBalanceComputed, transactionDetailData, currency, externalAssetOwner, savingsAccountNo); + } + } + + @Override + public Page retrieveAll(final SearchParameters searchParameters, final Long glAccountId, + final Boolean onlyManualEntries, final LocalDate fromDate, final LocalDate toDate, final LocalDate submittedOnDateFrom, + final LocalDate submittedOnDateTo, final String transactionId, final Integer entityType, + final JournalEntryAssociationParametersData associationParametersData) { + GLJournalEntryMapper rm = getGlJournalEntryMapper(associationParametersData); + final StringBuilder sqlBuilder = new StringBuilder(200); + sqlBuilder.append("select ").append(sqlGenerator.calcFoundRows()).append(" "); + sqlBuilder.append(rm.schema()); + + final Object[] objectArray = new Object[15]; + int arrayPos = 0; + String whereClose = " where "; + + if (StringUtils.isNotBlank(transactionId)) { + sqlBuilder.append(whereClose).append(" journalEntry.transaction_id = ?"); + objectArray[arrayPos] = transactionId; + arrayPos = arrayPos + 1; + + whereClose = " and "; + } + + if (entityType != null && entityType != 0 && (onlyManualEntries == null)) { + + sqlBuilder.append(whereClose).append(" journalEntry.entity_type_enum = ?"); + + objectArray[arrayPos] = entityType; + arrayPos = arrayPos + 1; + + whereClose = " and "; + } + + if (searchParameters.hasOfficeId()) { + sqlBuilder.append(whereClose).append(" journalEntry.office_id = ?"); + objectArray[arrayPos] = searchParameters.getOfficeId(); + arrayPos = arrayPos + 1; + + whereClose = " and "; + } + + if (searchParameters.hasCurrencyCode()) { + sqlBuilder.append(whereClose).append(" journalEntry.currency_code = ?"); + objectArray[arrayPos] = searchParameters.getCurrencyCode(); + arrayPos = arrayPos + 1; + + whereClose = " and "; + } + + if (glAccountId != null && glAccountId != 0) { + sqlBuilder.append(whereClose).append(" journalEntry.account_id = ?"); + objectArray[arrayPos] = glAccountId; + arrayPos = arrayPos + 1; + + whereClose = " and "; + } + + if (fromDate != null || toDate != null) { + if (fromDate != null && toDate != null) { + sqlBuilder.append(whereClose).append(" journalEntry.entry_date between ? and ? "); + whereClose = " and "; + objectArray[arrayPos] = fromDate; + arrayPos = arrayPos + 1; + objectArray[arrayPos] = toDate; + arrayPos = arrayPos + 1; + } else if (fromDate != null) { + sqlBuilder.append(whereClose).append(" journalEntry.entry_date >= ? "); + whereClose = " and "; + objectArray[arrayPos] = fromDate; + arrayPos = arrayPos + 1; + } else { + sqlBuilder.append(whereClose).append(" journalEntry.entry_date <= ? "); + whereClose = " and "; + objectArray[arrayPos] = toDate; + arrayPos = arrayPos + 1; + } + } + + if (submittedOnDateFrom != null || submittedOnDateTo != null) { + if (submittedOnDateFrom != null && submittedOnDateTo != null) { + sqlBuilder.append(whereClose).append(" journalEntry.submitted_on_date between ? and ? "); + whereClose = " and "; + objectArray[arrayPos] = submittedOnDateFrom; + arrayPos = arrayPos + 1; + objectArray[arrayPos] = submittedOnDateTo; + arrayPos = arrayPos + 1; + } else if (fromDate != null) { + sqlBuilder.append(whereClose).append(" journalEntry.submitted_on_date >= ? "); + whereClose = " and "; + objectArray[arrayPos] = submittedOnDateFrom; + arrayPos = arrayPos + 1; + } else { + sqlBuilder.append(whereClose).append(" journalEntry.submitted_on_date <= ? "); + whereClose = " and "; + objectArray[arrayPos] = submittedOnDateTo; + arrayPos = arrayPos + 1; + } + } + + if (onlyManualEntries != null) { + if (onlyManualEntries) { + sqlBuilder.append(whereClose).append(" journalEntry.manual_entry = ?"); + whereClose = " and "; + objectArray[arrayPos] = Boolean.TRUE; + arrayPos = arrayPos + 1; + } + } + + if (searchParameters.hasLoanId()) { + sqlBuilder.append(whereClose) + .append(" journalEntry.loan_transaction_id in (select id from m_loan_transaction where loan_id = ?)"); + objectArray[arrayPos] = searchParameters.getLoanId(); + arrayPos = arrayPos + 1; + + whereClose = " and "; + } + if (searchParameters.hasSavingsId()) { + sqlBuilder.append(whereClose).append( + " journalEntry.savings_transaction_id in (select id from m_savings_account_transaction where savings_account_id = ?)"); + objectArray[arrayPos] = searchParameters.getSavingsId(); + arrayPos = arrayPos + 1; + } + + 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.getOrderBy()); + } + } else { + sqlBuilder.append(" order by journalEntry.entry_date, journalEntry.id"); + } + + 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 = Arrays.copyOf(objectArray, arrayPos); + return this.paginationHelper.fetchPage(this.jdbcTemplate, sqlBuilder.toString(), finalObjectArray, rm); + } + + protected GLJournalEntryMapper getGlJournalEntryMapper(JournalEntryAssociationParametersData associationParametersData) { + return new GLJournalEntryMapper(associationParametersData); + } + + @Override + public JournalEntryData retrieveGLJournalEntryById(final long glJournalEntryId, + JournalEntryAssociationParametersData associationParametersData) { + try { + + final GLJournalEntryMapper rm = getGlJournalEntryMapper(associationParametersData); + // Programmatic query, disable sonar issue + final String sql = "select " + rm.schema() + " where journalEntry.id = ?"; + + return this.jdbcTemplate.queryForObject(sql, rm, glJournalEntryId); // NOSONAR + } catch (final EmptyResultDataAccessException e) { + throw new JournalEntriesNotFoundException(glJournalEntryId, e); + } + } + + @Override + public OfficeOpeningBalancesData retrieveOfficeOpeningBalances(final Long officeId, String currencyCode) { + + final FinancialActivityAccount financialActivityAccountId = this.financialActivityAccountRepositoryWrapper + .findByFinancialActivityTypeWithNotFoundDetection(300); + final Long contraId = financialActivityAccountId.getGlAccount().getId(); + if (contraId == null) { + throw new GeneralPlatformDomainRuleException( + "error.msg.financial.activity.mapping.opening.balance.contra.account.cannot.be.null", + "office-opening-balances-contra-account value can not be null", "office-opening-balances-contra-account"); + } + + final JournalEntryAssociationParametersData associationParametersData = new JournalEntryAssociationParametersData(); + final GLAccountData contraAccount = this.glAccountReadPlatformService.retrieveGLAccountById(contraId, associationParametersData); + if (!GLAccountType.fromInt(requireNonNull(requireNonNull(contraAccount.getType()).getId()).intValue()).isEquityType()) { + throw new GeneralPlatformDomainRuleException( + "error.msg.configuration.opening.balance.contra.account.value.is.invalid.account.type", + "Global configuration 'office-opening-balances-contra-account' value is not an equity type account", contraId); + } + + final OfficeData officeData = this.officeReadPlatformService.retrieveOffice(officeId); + final List allOpeningTransactions = populateAllTransactionsFromGLAccounts(contraId); + final String contraTransactionId = retrieveContraAccountTransactionId(officeId, contraId, currencyCode); + + List existingOpeningBalanceTransactions = new ArrayList<>(); + if (StringUtils.isNotBlank(contraTransactionId)) { + existingOpeningBalanceTransactions = retrieveOfficeBalanceTransactions(officeId, contraTransactionId, currencyCode); + } + final List transactions = populateOpeningBalances(existingOpeningBalanceTransactions, allOpeningTransactions); + final List assetAccountOpeningBalances = new ArrayList<>(); + final List liabilityAccountOpeningBalances = new ArrayList<>(); + final List incomeAccountOpeningBalances = new ArrayList<>(); + final List equityAccountOpeningBalances = new ArrayList<>(); + final List expenseAccountOpeningBalances = new ArrayList<>(); + + for (final JournalEntryData journalEntryData : transactions) { + final GLAccountType type = GLAccountType.fromInt(journalEntryData.getGlAccountType().getId().intValue()); + if (type.isAssetType()) { + assetAccountOpeningBalances.add(journalEntryData); + } else if (type.isLiabilityType()) { + liabilityAccountOpeningBalances.add(journalEntryData); + } else if (type.isEquityType()) { + equityAccountOpeningBalances.add(journalEntryData); + } else if (type.isIncomeType()) { + incomeAccountOpeningBalances.add(journalEntryData); + } else if (type.isExpenseType()) { + expenseAccountOpeningBalances.add(journalEntryData); + } + } + + final LocalDate transactionDate = DateUtils.getBusinessLocalDate(); + + return OfficeOpeningBalancesData.createNew(officeId, officeData.getName(), transactionDate, contraAccount, + assetAccountOpeningBalances, liabilityAccountOpeningBalances, incomeAccountOpeningBalances, equityAccountOpeningBalances, + expenseAccountOpeningBalances); + } + + private List populateOpeningBalances(final List existingOpeningBalanceTransactions, + final List allOpeningTransactions) { + final List allOpeningBalanceTransactions = new ArrayList<>(allOpeningTransactions.size()); + for (final JournalEntryData newOpeningBalanceTransaction : allOpeningTransactions) { + boolean isNewTransactionAddedToCollection = false; + for (final JournalEntryData existingOpeningBalanceTransaction : existingOpeningBalanceTransactions) { + if (newOpeningBalanceTransaction.getGlAccountId().equals(existingOpeningBalanceTransaction.getGlAccountId())) { + allOpeningBalanceTransactions.add(existingOpeningBalanceTransaction); + isNewTransactionAddedToCollection = true; + break; + } + } + if (!isNewTransactionAddedToCollection) { + allOpeningBalanceTransactions.add(newOpeningBalanceTransaction); + } + } + return allOpeningBalanceTransactions; + } + + private List populateAllTransactionsFromGLAccounts(final Long contraId) { + final List glAccounts = this.glAccountReadPlatformService.retrieveAllEnabledDetailGLAccounts(); + final List openingBalanceTransactions = new ArrayList<>(glAccounts.size()); + + for (final GLAccountData glAccountData : glAccounts) { + if (!contraId.equals(glAccountData.getId())) { + final JournalEntryData openingBalanceTransaction = JournalEntryData.fromGLAccountData(glAccountData); + openingBalanceTransactions.add(openingBalanceTransaction); + } + } + return openingBalanceTransactions; + } + + private List retrieveOfficeBalanceTransactions(final Long officeId, final String transactionId, + final String currencyCode) { + final Long contraId = null; + return retrieveContraTransactions(officeId, contraId, transactionId, currencyCode).getPageItems(); + } + + private String retrieveContraAccountTransactionId(final Long officeId, final Long contraId, final String currencyCode) { + final String transactionId = ""; + final Page contraJournalEntries = retrieveContraTransactions(officeId, contraId, transactionId, currencyCode); + if (!CollectionUtils.isEmpty(contraJournalEntries.getPageItems())) { + final JournalEntryData contraTransaction = contraJournalEntries.getPageItems() + .get(contraJournalEntries.getPageItems().size() - 1); + return contraTransaction.getTransactionId(); + } + return transactionId; + } + + private Page retrieveContraTransactions(final Long officeId, final Long contraId, final String transactionId, + final String currencyCode) { + final Integer entityType = null; + final Boolean onlyManualEntries = null; + final LocalDate fromDate = null; + final LocalDate toDate = null; + final LocalDate submittedOnDateFrom = null; + final LocalDate submittedOnDateTo = null; + final JournalEntryAssociationParametersData associationParametersData = null; + + final SearchParameters searchParameters = SearchParameters.builder().orphansOnly(false).officeId(officeId).offset(0) + .orderBy("journalEntry.id").sortOrder("ASC").currencyCode(currencyCode).build(); + + return retrieveAll(searchParameters, contraId, onlyManualEntries, fromDate, toDate, submittedOnDateFrom, submittedOnDateTo, + transactionId, entityType, associationParametersData); + + } + + @Override + public Page retrieveJournalEntriesByEntityId(String transactionId, Long entityId, Integer entityType) { + JournalEntryAssociationParametersData associationParametersData = new JournalEntryAssociationParametersData(true, true); + try { + final GLJournalEntryMapper rm = getGlJournalEntryMapper(associationParametersData); + final String sql = "select " + rm.schema() + + " where journalEntry.transaction_id = ? and journalEntry.entity_id = ? and journalEntry.entity_type_enum = ?"; + Object[] data = { transactionId, entityId, entityType }; + return this.paginationHelper.fetchPage(this.jdbcTemplate, sql, data, rm); + } catch (final EmptyResultDataAccessException e) { + throw new JournalEntriesNotFoundException(entityId, e); + } + } + + @Override + public GLAccountLedgerReportData retrieveGLAccountLedger(final Long glAccountId, final Long officeId, + final Boolean onlyManualEntries, final LocalDate fromDate, final LocalDate toDate, + final String transactionId, final SearchParameters searchParameters) { + + // ── 1. Fetch GL Account details ────────────────────────────────────── + GLAccountData glAccountData = null; + boolean isDebitNormal = true; // ASSET / EXPENSE = debit-normal + if (glAccountId != null && glAccountId > 0) { + glAccountData = this.glAccountReadPlatformService.retrieveGLAccountById(glAccountId, + new JournalEntryAssociationParametersData()); + if (glAccountData != null && glAccountData.getType() != null) { + final int typeId = glAccountData.getType().getId().intValue(); + // 1=ASSET, 2=LIABILITY, 3=EQUITY, 4=INCOME, 5=EXPENSE + isDebitNormal = (typeId == 1 || typeId == 5); + } + } + + // ── 2. Compute opening balance (all entries before fromDate) ───────── + BigDecimal openingBalance = BigDecimal.ZERO; + if (glAccountId != null && glAccountId > 0 && fromDate != null) { + final List obParams = new ArrayList<>(); + final StringBuilder obSql = new StringBuilder( + "SELECT COALESCE(SUM(CASE WHEN je.type_enum = 2 THEN je.amount ELSE 0 END), 0) AS ob_debit, " + + "COALESCE(SUM(CASE WHEN je.type_enum = 1 THEN je.amount ELSE 0 END), 0) AS ob_credit " + + "FROM acc_gl_journal_entry je " + + "WHERE je.account_id = ? AND je.entry_date < ?"); + obParams.add(glAccountId); + obParams.add(fromDate); + if (officeId != null && officeId > 0) { + obSql.append(" AND je.office_id = ?"); + obParams.add(officeId); + } + final BigDecimal[] ob = this.jdbcTemplate.queryForObject(obSql.toString(), + (rs, rowNum) -> new BigDecimal[]{ + Objects.requireNonNullElse(rs.getBigDecimal("ob_debit"), BigDecimal.ZERO), + Objects.requireNonNullElse(rs.getBigDecimal("ob_credit"), BigDecimal.ZERO) + }, obParams.toArray()); + if (ob != null) { + openingBalance = isDebitNormal ? ob[0].subtract(ob[1]) : ob[1].subtract(ob[0]); + } + } + + // ── 3. Day-wise summary for the period ─────────────────────────────── + final List dayWiseSummary = new ArrayList<>(); + { + final List dwParams = new ArrayList<>(); + final StringBuilder dwSql = new StringBuilder( + "SELECT je.entry_date AS dw_date, " + + "COALESCE(SUM(CASE WHEN je.type_enum = 2 THEN je.amount ELSE 0 END), 0) AS dw_debit, " + + "COALESCE(SUM(CASE WHEN je.type_enum = 1 THEN je.amount ELSE 0 END), 0) AS dw_credit, " + + "COUNT(*) AS dw_count " + + "FROM acc_gl_journal_entry je " + + "WHERE 1=1"); + if (glAccountId != null && glAccountId > 0) { + dwSql.append(" AND je.account_id = ?"); + dwParams.add(glAccountId); + } + if (officeId != null && officeId > 0) { + dwSql.append(" AND je.office_id = ?"); + dwParams.add(officeId); + } + if (fromDate != null) { + dwSql.append(" AND je.entry_date >= ?"); + dwParams.add(fromDate); + } + if (toDate != null) { + dwSql.append(" AND je.entry_date <= ?"); + dwParams.add(toDate); + } + if (onlyManualEntries != null && onlyManualEntries) { + dwSql.append(" AND je.manual_entry = true"); + } + dwSql.append(" GROUP BY je.entry_date ORDER BY je.entry_date ASC"); + + final boolean debitNormalFinal = isDebitNormal; + final List> rows = this.jdbcTemplate.queryForList(dwSql.toString(), dwParams.toArray()); + + BigDecimal running = openingBalance; + for (final Map row : rows) { + // Handle both java.sql.Date (older drivers) and java.time.LocalDate (newer drivers) + final Object rawDate = row.get("dw_date"); + LocalDate date; + if (rawDate instanceof LocalDate) { + date = (LocalDate) rawDate; + } else if (rawDate instanceof java.sql.Date) { + date = ((java.sql.Date) rawDate).toLocalDate(); + } else { + date = LocalDate.parse(String.valueOf(rawDate).substring(0, 10)); + } + final BigDecimal dr = Objects.requireNonNullElse((BigDecimal) row.get("dw_debit"), BigDecimal.ZERO); + final BigDecimal cr = Objects.requireNonNullElse((BigDecimal) row.get("dw_credit"), BigDecimal.ZERO); + final int count = ((Number) row.get("dw_count")).intValue(); + final BigDecimal net = debitNormalFinal ? dr.subtract(cr) : cr.subtract(dr); + final BigDecimal closing = running.add(net); + dayWiseSummary.add(DayWiseSummaryData.builder() + .date(date) + .openingBalance(running) + .totalDebit(dr) + .totalCredit(cr) + .netMovement(net) + .closingBalance(closing) + .entryCount(count) + .build()); + running = closing; + } + } + + // ── 4. Paginated journal entries — only fetch if limit > 0 ─────────── + final JournalEntryAssociationParametersData assocParams = new JournalEntryAssociationParametersData(true, true); + final int reqLimit = searchParameters != null && searchParameters.getLimit() != null + ? searchParameters.getLimit() : 0; + final Page entries; + if (reqLimit > 0) { + entries = retrieveAll(searchParameters, glAccountId, onlyManualEntries, + fromDate, toDate, null, null, transactionId, null, assocParams); + } else { + // Summary-only request — return empty page, no SQL needed + entries = new Page<>(new ArrayList<>(), 0); + } + + // ── 5. Compute period totals ───────────────────────────────────────── + BigDecimal totalDebit = BigDecimal.ZERO; + BigDecimal totalCredit = BigDecimal.ZERO; + { + final List totParams = new ArrayList<>(); + final StringBuilder totSql = new StringBuilder( + "SELECT COALESCE(SUM(CASE WHEN je.type_enum = 2 THEN je.amount ELSE 0 END), 0) AS totalDebit, " + + "COALESCE(SUM(CASE WHEN je.type_enum = 1 THEN je.amount ELSE 0 END), 0) AS totalCredit " + + "FROM acc_gl_journal_entry je WHERE 1=1"); + if (glAccountId != null && glAccountId > 0) { + totSql.append(" AND je.account_id = ?"); + totParams.add(glAccountId); + } + if (officeId != null && officeId > 0) { + totSql.append(" AND je.office_id = ?"); + totParams.add(officeId); + } + if (fromDate != null) { + totSql.append(" AND je.entry_date >= ?"); + totParams.add(fromDate); + } + if (toDate != null) { + totSql.append(" AND je.entry_date <= ?"); + totParams.add(toDate); + } + if (onlyManualEntries != null && onlyManualEntries) { + totSql.append(" AND je.manual_entry = true"); + } + final BigDecimal[] totResult = this.jdbcTemplate.queryForObject(totSql.toString(), + (rs, rowNum) -> new BigDecimal[]{ + Objects.requireNonNullElse(rs.getBigDecimal("totalDebit"), BigDecimal.ZERO), + Objects.requireNonNullElse(rs.getBigDecimal("totalCredit"), BigDecimal.ZERO) + }, totParams.toArray()); + if (totResult != null) { + totalDebit = totResult[0]; + totalCredit = totResult[1]; + } + } + + final BigDecimal netMovement = isDebitNormal ? totalDebit.subtract(totalCredit) : totalCredit.subtract(totalDebit); + final BigDecimal closingBalance = openingBalance.add(netMovement); + + // ── 6. Total entries count ──────────────────────────────────────────── + long totalEntries = 0L; + { + final List cntParams = new ArrayList<>(); + final StringBuilder cntSql = new StringBuilder( + "SELECT COUNT(*) FROM acc_gl_journal_entry je WHERE 1=1"); + if (glAccountId != null && glAccountId > 0) { + cntSql.append(" AND je.account_id = ?"); + cntParams.add(glAccountId); + } + if (officeId != null && officeId > 0) { + cntSql.append(" AND je.office_id = ?"); + cntParams.add(officeId); + } + if (fromDate != null) { + cntSql.append(" AND je.entry_date >= ?"); + cntParams.add(fromDate); + } + if (toDate != null) { + cntSql.append(" AND je.entry_date <= ?"); + cntParams.add(toDate); + } + if (onlyManualEntries != null && onlyManualEntries) { + cntSql.append(" AND je.manual_entry = true"); + } + final Long cnt = this.jdbcTemplate.queryForObject(cntSql.toString(), Long.class, cntParams.toArray()); + if (cnt != null) { totalEntries = cnt; } + } + + return GLAccountLedgerReportData.builder() + .glAccount(glAccountData) + .openingBalance(openingBalance) + .closingBalance(closingBalance) + .totalDebit(totalDebit) + .totalCredit(totalCredit) + .netMovement(netMovement) + .debitNormal(isDebitNormal) + .fromDate(fromDate) + .toDate(toDate) + .dayWiseSummary(dayWiseSummary) + .totalEntries(totalEntries) + .entries(entries) + .build(); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryRunningBalanceUpdateServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryRunningBalanceUpdateServiceImpl.java index d497f5fc3..8534da30d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryRunningBalanceUpdateServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryRunningBalanceUpdateServiceImpl.java @@ -1,285 +1,285 @@ -/** - * 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.accounting.journalentry.service; - -import java.math.BigDecimal; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.fineract.accounting.common.AccountingEnumerations; -import org.apache.fineract.accounting.glaccount.domain.GLAccountType; -import org.apache.fineract.accounting.journalentry.api.JournalEntryJsonInputParams; -import org.apache.fineract.accounting.journalentry.data.JournalEntryData; -import org.apache.fineract.accounting.journalentry.data.JournalEntryDataValidator; -import org.apache.fineract.accounting.journalentry.domain.JournalEntryType; -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.data.EnumOptionData; -import org.apache.fineract.infrastructure.core.domain.JdbcSupport; -import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; -import org.apache.fineract.infrastructure.core.service.DateUtils; -import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; -import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; -import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper; -import org.springframework.dao.EmptyResultDataAccessException; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.RowMapper; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -@Slf4j -public class JournalEntryRunningBalanceUpdateServiceImpl implements JournalEntryRunningBalanceUpdateService { - - private final JdbcTemplate jdbcTemplate; - - private final OfficeRepositoryWrapper officeRepositoryWrapper; - - private final JournalEntryDataValidator dataValidator; - - private final FromJsonHelper fromApiJsonHelper; - private final DatabaseSpecificSQLGenerator sqlGenerator; - - private final GLJournalEntryMapper entryMapper = new GLJournalEntryMapper(); - - private final PlatformSecurityContext platformSecurityContext; - - @Override - public void updateRunningBalance() { - String dateFinder = "select MIN(je.entry_date) as entityDate from acc_gl_journal_entry je " - + "where je.is_running_balance_calculated=false "; - try { - LocalDate entityDate = this.jdbcTemplate.queryForObject(dateFinder, LocalDate.class); - updateOrganizationRunningBalance(entityDate); - } catch (EmptyResultDataAccessException e) { - log.debug("No results found for updation of running balance "); - } - } - - @Override - public CommandProcessingResult updateOfficeRunningBalance(JsonCommand command) { - this.dataValidator.validateForUpdateRunningBalance(command); - final Long officeId = this.fromApiJsonHelper.extractLongNamed(JournalEntryJsonInputParams.OFFICE_ID.getValue(), - command.parsedJson()); - CommandProcessingResultBuilder commandProcessingResultBuilder = new CommandProcessingResultBuilder() - .withCommandId(command.commandId()); - if (officeId == null) { - updateRunningBalance(); - } else { - this.officeRepositoryWrapper.findOneWithNotFoundDetection(officeId); - String dateFinder = "select MIN(je.entry_date) as entityDate " + "from acc_gl_journal_entry je " - + "where je.is_running_balance_calculated=false and je.office_id=?"; - try { - LocalDate entityDate = this.jdbcTemplate.queryForObject(dateFinder, LocalDate.class, officeId); - updateRunningBalance(officeId, entityDate); - } catch (EmptyResultDataAccessException e) { - log.debug("No results found for updation of office running balance with office id: {}", officeId); - } - commandProcessingResultBuilder.withOfficeId(officeId); - } - return commandProcessingResultBuilder.build(); - } - - private void updateOrganizationRunningBalance(LocalDate entityDate) { - Map runningBalanceMap = new HashMap<>(5); - Map> officesRunningBalance = new HashMap<>(); - - final String organizationRunningBalanceQuery = "select je.organization_running_balance as runningBalance,je.account_id as accountId from acc_gl_journal_entry je " - + "inner join (select max(id) as id from acc_gl_journal_entry where entry_date < ? group by account_id,entry_date) je2 ON je2.id = je.id " - + "inner join (select max(entry_date) as date from acc_gl_journal_entry where entry_date < ? group by account_id) je3 ON je.entry_date = je3.date " - + "group by je.id order by je.entry_date DESC " + sqlGenerator.limit(10000, 0); - - List> list = jdbcTemplate.queryForList(organizationRunningBalanceQuery, // NOSONAR - entityDate, entityDate); - - for (Map entries : list) { - Long accountId = Long.parseLong(entries.get("accountId").toString()); // Drizzle - // is - // returning - // Big - // Integer - // where - // as - // MySQL - // returns - // Long. - if (!runningBalanceMap.containsKey(accountId)) { - runningBalanceMap.put(accountId, (BigDecimal) entries.get("runningBalance")); - } - } - - final String offlineRunningBalanceQuery = "select je.office_running_balance as runningBalance,je.account_id as accountId,je.office_id as officeId " - + "from acc_gl_journal_entry je " - + "inner join (select max(id) as id from acc_gl_journal_entry where entry_date < ? group by office_id,account_id,entry_date) je2 ON je2.id = je.id " - + "inner join (select max(entry_date) as date from acc_gl_journal_entry where entry_date < ? group by office_id,account_id) je3 ON je.entry_date = je3.date " - + "group by je.id order by je.entry_date DESC " + sqlGenerator.limit(10000, 0); - - List> officesRunningBalanceList = jdbcTemplate.queryForList(offlineRunningBalanceQuery, // NOSONAR - entityDate, entityDate); - for (Map entries : officesRunningBalanceList) { - Long accountId = Long.parseLong(entries.get("accountId").toString()); - Long officeId = Long.parseLong(entries.get("officeId").toString()); - Map runningBalance; - if (officesRunningBalance.containsKey(officeId)) { - runningBalance = officesRunningBalance.get(officeId); - } else { - runningBalance = new HashMap<>(); - officesRunningBalance.put(officeId, runningBalance); - } - if (!runningBalance.containsKey(accountId)) { - runningBalance.put(accountId, (BigDecimal) entries.get("runningBalance")); - } - } - - List entryDataList = jdbcTemplate.query(entryMapper.organizationRunningBalanceSchema(), entryMapper, entityDate); - if (entryDataList.size() > 0) { - // run a batch update of 1000 SQL statements at a time - final int batchUpdateSize = 1000; - List params = new ArrayList<>(); - int batchIndex = 0; - String sql = "UPDATE acc_gl_journal_entry SET is_running_balance_calculated=?, organization_running_balance=?," - + "office_running_balance=?, last_modified_by=?, last_modified_on_utc=? WHERE id=?"; - for (int index = 0; index < entryDataList.size(); index++) { - JournalEntryData entryData = entryDataList.get(index); - Map officeRunningBalanceMap; - if (officesRunningBalance.containsKey(entryData.getOfficeId())) { - officeRunningBalanceMap = officesRunningBalance.get(entryData.getOfficeId()); - } else { - officeRunningBalanceMap = new HashMap<>(); - officesRunningBalance.put(entryData.getOfficeId(), officeRunningBalanceMap); - } - BigDecimal officeRunningBalance = calculateRunningBalance(entryData, officeRunningBalanceMap); - BigDecimal runningBalance = calculateRunningBalance(entryData, runningBalanceMap); - - params.add(new Object[] { Boolean.TRUE, runningBalance, officeRunningBalance, - platformSecurityContext.authenticatedUser().getId(), DateUtils.getAuditOffsetDateTime(), entryData.getId() }); - batchIndex++; - if (batchIndex == batchUpdateSize || index == entryDataList.size() - 1) { - this.jdbcTemplate.batchUpdate(sql, params); - // reset counter and string array - batchIndex = 0; - params.clear(); - } - } - } - - } - - private void updateRunningBalance(Long officeId, LocalDate entityDate) { - Map runningBalanceMap = new HashMap<>(5); - - final String offlineRunningBalanceQuery = "select je.office_running_balance as runningBalance,je.account_id as accountId from acc_gl_journal_entry je " - + "inner join (select max(id) as id from acc_gl_journal_entry where office_id=? and entry_date < ? group by account_id,entry_date) je2 ON je2.id = je.id " - + "inner join (select max(entry_date) as date from acc_gl_journal_entry where office_id=? and entry_date < ? group by account_id) je3 ON je.entry_date = je3.date " - + "group by je.id order by je.entry_date DESC " + sqlGenerator.limit(10000, 0); - - List> list = jdbcTemplate.queryForList(offlineRunningBalanceQuery, // NOSONAR - officeId, entityDate, officeId, entityDate); - for (Map entries : list) { - Long accountId = (Long) entries.get("accountId"); - if (!runningBalanceMap.containsKey(accountId)) { - runningBalanceMap.put(accountId, (BigDecimal) entries.get("runningBalance")); - } - } - List entryDataList = jdbcTemplate.query(entryMapper.officeRunningBalanceSchema(), entryMapper, officeId, - entityDate); - List params = new ArrayList<>(); - - String sql = "UPDATE acc_gl_journal_entry SET office_running_balance=?, last_modified_by=?, last_modified_on_utc=? WHERE id=?"; - for (JournalEntryData entryData : entryDataList) { - BigDecimal runningBalance = calculateRunningBalance(entryData, runningBalanceMap); - params.add(new Object[] { runningBalance, platformSecurityContext.authenticatedUser().getId(), - DateUtils.getAuditOffsetDateTime(), entryData.getId() }); - } - this.jdbcTemplate.batchUpdate(sql, params); - } - - private BigDecimal calculateRunningBalance(JournalEntryData entry, Map runningBalanceMap) { - BigDecimal runningBalance = BigDecimal.ZERO; - if (runningBalanceMap.containsKey(entry.getGlAccountId())) { - runningBalance = runningBalanceMap.get(entry.getGlAccountId()); - } - GLAccountType accountType = GLAccountType.fromInt(entry.getGlAccountType().getId().intValue()); - JournalEntryType entryType = JournalEntryType.fromInt(entry.getEntryType().getId().intValue()); - boolean isIncrease = false; - switch (accountType) { - case ASSET: - case EXPENSE: - if (entryType.isDebitType()) { - isIncrease = true; - } - break; - case EQUITY: - case INCOME: - case LIABILITY: - if (entryType.isCreditType()) { - isIncrease = true; - } - break; - } - if (isIncrease) { - runningBalance = runningBalance.add(entry.getAmount()); - } else { - runningBalance = runningBalance.subtract(entry.getAmount()); - } - runningBalanceMap.put(entry.getGlAccountId(), runningBalance); - return runningBalance; - } - - private static final class GLJournalEntryMapper implements RowMapper { - - public String officeRunningBalanceSchema() { - return "select je.id as id,je.account_id as glAccountId,je.type_enum as entryType,je.amount as amount, " - + "glAccount.classification_enum as classification,je.office_id as officeId " - + "from acc_gl_journal_entry je , acc_gl_account glAccount " + "where je.account_id = glAccount.id " - + "and je.office_id=? and je.entry_date >= ? order by je.entry_date,je.id"; - } - - public String organizationRunningBalanceSchema() { - return "select je.id as id,je.account_id as glAccountId," + "je.type_enum as entryType,je.amount as amount, " - + "glAccount.classification_enum as classification,je.office_id as officeId " - + "from acc_gl_journal_entry je , acc_gl_account glAccount " + "where je.account_id = glAccount.id " - + "and je.entry_date >= ? order by je.entry_date,je.id"; - } - - @Override - public JournalEntryData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { - - final Long id = rs.getLong("id"); - final Long glAccountId = rs.getLong("glAccountId"); - final Long officeId = rs.getLong("officeId"); - final int accountTypeId = JdbcSupport.getInteger(rs, "classification"); - final EnumOptionData accountType = AccountingEnumerations.gLAccountType(accountTypeId); - final BigDecimal amount = rs.getBigDecimal("amount"); - final int entryTypeId = JdbcSupport.getInteger(rs, "entryType"); - final EnumOptionData entryType = AccountingEnumerations.journalEntryType(entryTypeId); - - return new JournalEntryData(id, officeId, null, null, glAccountId, null, accountType, null, entryType, amount, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null); - } - } - -} +/** + * 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.accounting.journalentry.service; + +import java.math.BigDecimal; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.accounting.common.AccountingEnumerations; +import org.apache.fineract.accounting.glaccount.domain.GLAccountType; +import org.apache.fineract.accounting.journalentry.api.JournalEntryJsonInputParams; +import org.apache.fineract.accounting.journalentry.data.JournalEntryData; +import org.apache.fineract.accounting.journalentry.data.JournalEntryDataValidator; +import org.apache.fineract.accounting.journalentry.domain.JournalEntryType; +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.data.EnumOptionData; +import org.apache.fineract.infrastructure.core.domain.JdbcSupport; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@Slf4j +public class JournalEntryRunningBalanceUpdateServiceImpl implements JournalEntryRunningBalanceUpdateService { + + private final JdbcTemplate jdbcTemplate; + + private final OfficeRepositoryWrapper officeRepositoryWrapper; + + private final JournalEntryDataValidator dataValidator; + + private final FromJsonHelper fromApiJsonHelper; + private final DatabaseSpecificSQLGenerator sqlGenerator; + + private final GLJournalEntryMapper entryMapper = new GLJournalEntryMapper(); + + private final PlatformSecurityContext platformSecurityContext; + + @Override + public void updateRunningBalance() { + String dateFinder = "select MIN(je.entry_date) as entityDate from acc_gl_journal_entry je " + + "where je.is_running_balance_calculated=false "; + try { + LocalDate entityDate = this.jdbcTemplate.queryForObject(dateFinder, LocalDate.class); + updateOrganizationRunningBalance(entityDate); + } catch (EmptyResultDataAccessException e) { + log.debug("No results found for updation of running balance "); + } + } + + @Override + public CommandProcessingResult updateOfficeRunningBalance(JsonCommand command) { + this.dataValidator.validateForUpdateRunningBalance(command); + final Long officeId = this.fromApiJsonHelper.extractLongNamed(JournalEntryJsonInputParams.OFFICE_ID.getValue(), + command.parsedJson()); + CommandProcessingResultBuilder commandProcessingResultBuilder = new CommandProcessingResultBuilder() + .withCommandId(command.commandId()); + if (officeId == null) { + updateRunningBalance(); + } else { + this.officeRepositoryWrapper.findOneWithNotFoundDetection(officeId); + String dateFinder = "select MIN(je.entry_date) as entityDate " + "from acc_gl_journal_entry je " + + "where je.is_running_balance_calculated=false and je.office_id=?"; + try { + LocalDate entityDate = this.jdbcTemplate.queryForObject(dateFinder, LocalDate.class, officeId); + updateRunningBalance(officeId, entityDate); + } catch (EmptyResultDataAccessException e) { + log.debug("No results found for updation of office running balance with office id: {}", officeId); + } + commandProcessingResultBuilder.withOfficeId(officeId); + } + return commandProcessingResultBuilder.build(); + } + + private void updateOrganizationRunningBalance(LocalDate entityDate) { + Map runningBalanceMap = new HashMap<>(5); + Map> officesRunningBalance = new HashMap<>(); + + final String organizationRunningBalanceQuery = "select je.organization_running_balance as runningBalance,je.account_id as accountId from acc_gl_journal_entry je " + + "inner join (select max(id) as id from acc_gl_journal_entry where entry_date < ? group by account_id,entry_date) je2 ON je2.id = je.id " + + "inner join (select max(entry_date) as date from acc_gl_journal_entry where entry_date < ? group by account_id) je3 ON je.entry_date = je3.date " + + "group by je.id order by je.entry_date DESC " + sqlGenerator.limit(10000, 0); + + List> list = jdbcTemplate.queryForList(organizationRunningBalanceQuery, // NOSONAR + entityDate, entityDate); + + for (Map entries : list) { + Long accountId = Long.parseLong(entries.get("accountId").toString()); // Drizzle + // is + // returning + // Big + // Integer + // where + // as + // MySQL + // returns + // Long. + if (!runningBalanceMap.containsKey(accountId)) { + runningBalanceMap.put(accountId, (BigDecimal) entries.get("runningBalance")); + } + } + + final String offlineRunningBalanceQuery = "select je.office_running_balance as runningBalance,je.account_id as accountId,je.office_id as officeId " + + "from acc_gl_journal_entry je " + + "inner join (select max(id) as id from acc_gl_journal_entry where entry_date < ? group by office_id,account_id,entry_date) je2 ON je2.id = je.id " + + "inner join (select max(entry_date) as date from acc_gl_journal_entry where entry_date < ? group by office_id,account_id) je3 ON je.entry_date = je3.date " + + "group by je.id order by je.entry_date DESC " + sqlGenerator.limit(10000, 0); + + List> officesRunningBalanceList = jdbcTemplate.queryForList(offlineRunningBalanceQuery, // NOSONAR + entityDate, entityDate); + for (Map entries : officesRunningBalanceList) { + Long accountId = Long.parseLong(entries.get("accountId").toString()); + Long officeId = Long.parseLong(entries.get("officeId").toString()); + Map runningBalance; + if (officesRunningBalance.containsKey(officeId)) { + runningBalance = officesRunningBalance.get(officeId); + } else { + runningBalance = new HashMap<>(); + officesRunningBalance.put(officeId, runningBalance); + } + if (!runningBalance.containsKey(accountId)) { + runningBalance.put(accountId, (BigDecimal) entries.get("runningBalance")); + } + } + + List entryDataList = jdbcTemplate.query(entryMapper.organizationRunningBalanceSchema(), entryMapper, entityDate); + if (entryDataList.size() > 0) { + // run a batch update of 1000 SQL statements at a time + final int batchUpdateSize = 1000; + List params = new ArrayList<>(); + int batchIndex = 0; + String sql = "UPDATE acc_gl_journal_entry SET is_running_balance_calculated=?, organization_running_balance=?," + + "office_running_balance=?, last_modified_by=?, last_modified_on_utc=? WHERE id=?"; + for (int index = 0; index < entryDataList.size(); index++) { + JournalEntryData entryData = entryDataList.get(index); + Map officeRunningBalanceMap; + if (officesRunningBalance.containsKey(entryData.getOfficeId())) { + officeRunningBalanceMap = officesRunningBalance.get(entryData.getOfficeId()); + } else { + officeRunningBalanceMap = new HashMap<>(); + officesRunningBalance.put(entryData.getOfficeId(), officeRunningBalanceMap); + } + BigDecimal officeRunningBalance = calculateRunningBalance(entryData, officeRunningBalanceMap); + BigDecimal runningBalance = calculateRunningBalance(entryData, runningBalanceMap); + + params.add(new Object[] { Boolean.TRUE, runningBalance, officeRunningBalance, + platformSecurityContext.authenticatedUser().getId(), DateUtils.getAuditOffsetDateTime(), entryData.getId() }); + batchIndex++; + if (batchIndex == batchUpdateSize || index == entryDataList.size() - 1) { + this.jdbcTemplate.batchUpdate(sql, params); + // reset counter and string array + batchIndex = 0; + params.clear(); + } + } + } + + } + + private void updateRunningBalance(Long officeId, LocalDate entityDate) { + Map runningBalanceMap = new HashMap<>(5); + + final String offlineRunningBalanceQuery = "select je.office_running_balance as runningBalance,je.account_id as accountId from acc_gl_journal_entry je " + + "inner join (select max(id) as id from acc_gl_journal_entry where office_id=? and entry_date < ? group by account_id,entry_date) je2 ON je2.id = je.id " + + "inner join (select max(entry_date) as date from acc_gl_journal_entry where office_id=? and entry_date < ? group by account_id) je3 ON je.entry_date = je3.date " + + "group by je.id order by je.entry_date DESC " + sqlGenerator.limit(10000, 0); + + List> list = jdbcTemplate.queryForList(offlineRunningBalanceQuery, // NOSONAR + officeId, entityDate, officeId, entityDate); + for (Map entries : list) { + Long accountId = (Long) entries.get("accountId"); + if (!runningBalanceMap.containsKey(accountId)) { + runningBalanceMap.put(accountId, (BigDecimal) entries.get("runningBalance")); + } + } + List entryDataList = jdbcTemplate.query(entryMapper.officeRunningBalanceSchema(), entryMapper, officeId, + entityDate); + List params = new ArrayList<>(); + + String sql = "UPDATE acc_gl_journal_entry SET office_running_balance=?, last_modified_by=?, last_modified_on_utc=? WHERE id=?"; + for (JournalEntryData entryData : entryDataList) { + BigDecimal runningBalance = calculateRunningBalance(entryData, runningBalanceMap); + params.add(new Object[] { runningBalance, platformSecurityContext.authenticatedUser().getId(), + DateUtils.getAuditOffsetDateTime(), entryData.getId() }); + } + this.jdbcTemplate.batchUpdate(sql, params); + } + + private BigDecimal calculateRunningBalance(JournalEntryData entry, Map runningBalanceMap) { + BigDecimal runningBalance = BigDecimal.ZERO; + if (runningBalanceMap.containsKey(entry.getGlAccountId())) { + runningBalance = runningBalanceMap.get(entry.getGlAccountId()); + } + GLAccountType accountType = GLAccountType.fromInt(entry.getGlAccountType().getId().intValue()); + JournalEntryType entryType = JournalEntryType.fromInt(entry.getEntryType().getId().intValue()); + boolean isIncrease = false; + switch (accountType) { + case ASSET: + case EXPENSE: + if (entryType.isDebitType()) { + isIncrease = true; + } + break; + case EQUITY: + case INCOME: + case LIABILITY: + if (entryType.isCreditType()) { + isIncrease = true; + } + break; + } + if (isIncrease) { + runningBalance = runningBalance.add(entry.getAmount()); + } else { + runningBalance = runningBalance.subtract(entry.getAmount()); + } + runningBalanceMap.put(entry.getGlAccountId(), runningBalance); + return runningBalance; + } + + private static final class GLJournalEntryMapper implements RowMapper { + + public String officeRunningBalanceSchema() { + return "select je.id as id,je.account_id as glAccountId,je.type_enum as entryType,je.amount as amount, " + + "glAccount.classification_enum as classification,je.office_id as officeId " + + "from acc_gl_journal_entry je , acc_gl_account glAccount " + "where je.account_id = glAccount.id " + + "and je.office_id=? and je.entry_date >= ? order by je.entry_date,je.id"; + } + + public String organizationRunningBalanceSchema() { + return "select je.id as id,je.account_id as glAccountId," + "je.type_enum as entryType,je.amount as amount, " + + "glAccount.classification_enum as classification,je.office_id as officeId " + + "from acc_gl_journal_entry je , acc_gl_account glAccount " + "where je.account_id = glAccount.id " + + "and je.entry_date >= ? order by je.entry_date,je.id"; + } + + @Override + public JournalEntryData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { + + final Long id = rs.getLong("id"); + final Long glAccountId = rs.getLong("glAccountId"); + final Long officeId = rs.getLong("officeId"); + final int accountTypeId = JdbcSupport.getInteger(rs, "classification"); + final EnumOptionData accountType = AccountingEnumerations.gLAccountType(accountTypeId); + final BigDecimal amount = rs.getBigDecimal("amount"); + final int entryTypeId = JdbcSupport.getInteger(rs, "entryType"); + final EnumOptionData entryType = AccountingEnumerations.journalEntryType(entryTypeId); + + return new JournalEntryData(id, officeId, null, null, glAccountId, null, accountType, null, entryType, amount, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null); + } + } + +} 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 70d719452..60dfa4ec3 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 @@ -1,4043 +1,4044 @@ -/** - * 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 java.time.temporal.ChronoUnit.DAYS; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.allowOverdraftParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.dueAsOfDateParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.enforceMinRequiredBalanceParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.lienAllowedParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.localeParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.lockinPeriodFrequencyParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.lockinPeriodFrequencyTypeParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.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.nominalAnnualInterestRateOverdraftParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.overdraftLimitParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.withHoldTaxParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.withdrawalFeeForTransfersParamName; - -import com.google.gson.JsonArray; -import jakarta.persistence.CascadeType; -import jakarta.persistence.Column; -import jakarta.persistence.DiscriminatorColumn; -import jakarta.persistence.DiscriminatorType; -import jakarta.persistence.DiscriminatorValue; -import jakarta.persistence.Embedded; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.Inheritance; -import jakarta.persistence.InheritanceType; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.OneToMany; -import jakarta.persistence.OrderBy; -import jakarta.persistence.Table; -import jakarta.persistence.Transient; -import jakarta.persistence.UniqueConstraint; -import jakarta.persistence.Version; -import java.math.BigDecimal; -import java.math.MathContext; -import java.time.LocalDate; -import java.time.YearMonth; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.Arrays; -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 java.util.stream.Collectors; -import org.apache.commons.lang3.StringUtils; -import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; -import org.apache.fineract.infrastructure.configuration.service.TemporaryConfigurationServiceContainer; -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.AbstractAuditableWithUTCDateTimeCustom; -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.infrastructure.core.service.ExternalIdFactory; -import org.apache.fineract.infrastructure.core.service.MathUtil; -import org.apache.fineract.infrastructure.security.service.RandomPasswordGenerator; -import org.apache.fineract.interoperation.domain.InteropIdentifier; -import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; -import org.apache.fineract.organisation.monetary.domain.Money; -import org.apache.fineract.organisation.monetary.domain.MoneyHelper; -import org.apache.fineract.organisation.office.domain.Office; -import org.apache.fineract.organisation.staff.domain.Staff; -import org.apache.fineract.portfolio.accountdetails.domain.AccountType; -import org.apache.fineract.portfolio.charge.domain.Charge; -import org.apache.fineract.portfolio.charge.exception.SavingsAccountChargeNotFoundException; -import org.apache.fineract.portfolio.client.domain.Client; -import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; -import org.apache.fineract.portfolio.group.domain.Group; -import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail; -import org.apache.fineract.portfolio.savings.DepositAccountType; -import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType; -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.exception.InsufficientAccountBalanceException; -import org.apache.fineract.portfolio.savings.exception.SavingsAccountBlockedException; -import org.apache.fineract.portfolio.savings.exception.SavingsAccountCreditsBlockedException; -import org.apache.fineract.portfolio.savings.exception.SavingsAccountDebitsBlockedException; -import org.apache.fineract.portfolio.savings.exception.SavingsAccountTransactionNotFoundException; -import org.apache.fineract.portfolio.savings.exception.SavingsActivityPriorToClientTransferException; -import org.apache.fineract.portfolio.savings.exception.SavingsOfficerAssignmentDateException; -import org.apache.fineract.portfolio.savings.exception.SavingsOfficerUnassignmentDateException; -import org.apache.fineract.portfolio.savings.exception.SavingsTransferTransactionsCannotBeUndoneException; -import org.apache.fineract.portfolio.savings.service.SavingsEnumerations; -import org.apache.fineract.portfolio.tax.domain.TaxComponent; -import org.apache.fineract.portfolio.tax.domain.TaxGroup; -import org.apache.fineract.portfolio.tax.service.TaxUtils; -import org.apache.fineract.useradministration.domain.AppUser; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.util.CollectionUtils; - -@Entity -@Table(name = "m_savings_account", uniqueConstraints = { @UniqueConstraint(columnNames = { "account_no" }, name = "sa_account_no_UNIQUE"), - @UniqueConstraint(columnNames = { "external_id" }, name = "sa_external_id_UNIQUE") }) -@Inheritance(strategy = InheritanceType.SINGLE_TABLE) -@DiscriminatorColumn(name = "deposit_type_enum", discriminatorType = DiscriminatorType.INTEGER) -@DiscriminatorValue("100") -@SuppressWarnings({ "MemberName" }) -public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom implements IDepositAccountType { - - private static final Logger LOG = LoggerFactory.getLogger(SavingsAccount.class); - - @Version - int version; - - @Column(name = "account_no", length = 20, unique = true, nullable = false) - protected String accountNumber; - - @Column(name = "external_id", nullable = true) - protected ExternalId externalId; - - @ManyToOne(optional = true) - @JoinColumn(name = "client_id", nullable = true) - protected Client client; - - @ManyToOne(optional = true, fetch = FetchType.LAZY) - @JoinColumn(name = "group_id", nullable = true) - protected Group group; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "gsim_id", nullable = true) - private GroupSavingsIndividualMonitoring gsim; - - @ManyToOne - @JoinColumn(name = "product_id", nullable = false) - protected SavingsProduct product; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "field_officer_id", nullable = true) - protected Staff savingsOfficer; - - @Column(name = "status_enum", nullable = false) - protected Integer status; - - @Column(name = "sub_status_enum", nullable = false) - protected Integer sub_status = 0; - - @Column(name = "account_type_enum", nullable = false) - protected Integer accountType; - - @Column(name = "submittedon_date", nullable = true) - protected LocalDate submittedOnDate; - - @ManyToOne(optional = true, fetch = FetchType.LAZY) - @JoinColumn(name = "submittedon_userid", nullable = true) - protected AppUser submittedBy; - - @Column(name = "rejectedon_date") - protected LocalDate rejectedOnDate; - - @ManyToOne(optional = true, fetch = FetchType.LAZY) - @JoinColumn(name = "rejectedon_userid", nullable = true) - protected AppUser rejectedBy; - - @Column(name = "withdrawnon_date") - protected LocalDate withdrawnOnDate; - - @ManyToOne(optional = true, fetch = FetchType.LAZY) - @JoinColumn(name = "withdrawnon_userid", nullable = true) - protected AppUser withdrawnBy; - - @Column(name = "approvedon_date") - protected LocalDate approvedOnDate; - - @ManyToOne(optional = true, fetch = FetchType.LAZY) - @JoinColumn(name = "approvedon_userid", nullable = true) - protected AppUser approvedBy; - - @Column(name = "activatedon_date", nullable = true) - protected LocalDate activatedOnDate; - - @ManyToOne(optional = true, fetch = FetchType.LAZY) - @JoinColumn(name = "activatedon_userid", nullable = true) - protected AppUser activatedBy; - - @Column(name = "closedon_date") - protected LocalDate closedOnDate; - - @ManyToOne(optional = true, fetch = FetchType.LAZY) - @JoinColumn(name = "closedon_userid", nullable = true) - protected AppUser closedBy; - - @Column(name = "reason_for_block", nullable = true) - protected String reasonForBlock; - - @Embedded - protected MonetaryCurrency currency; - - @Column(name = "nominal_annual_interest_rate", scale = 6, precision = 19, nullable = false) - protected BigDecimal nominalAnnualInterestRate; - - /** - * The interest period is the span of time at the end of which savings in a client's account earn interest. - * - * A value from the {@link SavingsCompoundingInterestPeriodType} enumeration. - */ - @Column(name = "interest_compounding_period_enum", nullable = false) - protected Integer interestCompoundingPeriodType; - - /** - * A value from the {@link SavingsPostingInterestPeriodType} enumeration. - */ - @Column(name = "interest_posting_period_enum", nullable = false) - protected Integer interestPostingPeriodType; - - /** - * A value from the {@link SavingsInterestCalculationType} enumeration. - */ - @Column(name = "interest_calculation_type_enum", nullable = false) - protected Integer interestCalculationType; - - /** - * A value from the {@link SavingsInterestCalculationDaysInYearType} enumeration. - */ - @Column(name = "interest_calculation_days_in_year_type_enum", nullable = false) - protected Integer interestCalculationDaysInYearType; - - @Column(name = "min_required_opening_balance", scale = 6, precision = 19, nullable = true) - protected BigDecimal minRequiredOpeningBalance; - - @Column(name = "lockin_period_frequency", nullable = true) - protected Integer lockinPeriodFrequency; - - @Column(name = "lockin_period_frequency_enum", nullable = true) - protected Integer lockinPeriodFrequencyType; - - /** - * When account becomes active this field is derived if lockinPeriodFrequency and - * lockinPeriodFrequencyType details are present. - */ - @Column(name = "lockedin_until_date_derived", nullable = true) - protected LocalDate lockedInUntilDate; - - @Column(name = "withdrawal_fee_for_transfer", nullable = true) - protected boolean withdrawalFeeApplicableForTransfer; - - @Column(name = "allow_overdraft") - private boolean allowOverdraft; - - @Column(name = "overdraft_limit", scale = 6, precision = 19, nullable = true) - private BigDecimal overdraftLimit; - - @Column(name = "nominal_annual_interest_rate_overdraft", scale = 6, precision = 19, nullable = true) - protected BigDecimal nominalAnnualInterestRateOverdraft; - - @Column(name = "min_overdraft_for_interest_calculation", scale = 6, precision = 19, nullable = true) - private BigDecimal minOverdraftForInterestCalculation; - - @Column(name = "enforce_min_required_balance") - private boolean enforceMinRequiredBalance; - - @Column(name = "min_required_balance", scale = 6, precision = 19, nullable = true) - private BigDecimal minRequiredBalance; - - @Column(name = "is_lien_allowed", nullable = false) - private boolean lienAllowed; - - @Column(name = "max_allowed_lien_limit", scale = 6, precision = 19, nullable = true) - private BigDecimal maxAllowedLienLimit; - - @Column(name = "on_hold_funds_derived", scale = 6, precision = 19, nullable = true) - private BigDecimal onHoldFunds; - - @Column(name = "start_interest_calculation_date") - protected LocalDate startInterestCalculationDate; - - @Embedded - protected SavingsAccountSummary summary; - - @OrderBy(value = "dateOf, createdDate, id") - @OneToMany(cascade = CascadeType.ALL, mappedBy = "savingsAccount", orphanRemoval = true, fetch = FetchType.LAZY) - protected List transactions = new ArrayList<>(); - - @OneToMany(cascade = CascadeType.ALL, mappedBy = "savingsAccount", orphanRemoval = true, fetch = FetchType.LAZY) - protected Set charges = new HashSet<>(); - - @OneToMany(cascade = CascadeType.ALL, mappedBy = "savingsAccount", orphanRemoval = true, fetch = FetchType.LAZY) - private Set savingsOfficerHistory = new HashSet<>(); - - @Transient - protected boolean accountNumberRequiresAutoGeneration = false; - @Transient - protected SavingsAccountTransactionSummaryWrapper savingsAccountTransactionSummaryWrapper; - @Transient - protected SavingsHelper savingsHelper; - @Transient - protected List savingsAccountTransactions = new ArrayList<>(); - - @Column(name = "deposit_type_enum", insertable = false, updatable = false) - private Integer depositType; - - @Column(name = "min_balance_for_interest_calculation", scale = 6, precision = 19, nullable = true) - private BigDecimal minBalanceForInterestCalculation; - - @Column(name = "withhold_tax", nullable = false) - protected boolean withHoldTax; - - @ManyToOne - @JoinColumn(name = "tax_group_id") - private TaxGroup taxGroup; - - @Column(name = "accrued_till_date") - private LocalDate accruedTillDate; - - @Column(name = "last_closed_business_date") - private LocalDate lastClosedBusinessDate; - - @Column(name = "total_savings_amount_on_hold", scale = 6, precision = 19, nullable = true) - private BigDecimal savingsOnHoldAmount; - @OneToMany(cascade = CascadeType.ALL, mappedBy = "account", orphanRemoval = true, fetch = FetchType.LAZY) - protected List identifiers = new ArrayList<>(); - - public transient ConfigurationDomainService configurationDomainService; - - protected SavingsAccount() { - // - } - - public static SavingsAccount 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 boolean allowOverdraft, final BigDecimal overdraftLimit, final boolean enforceMinRequiredBalance, - final BigDecimal minRequiredBalance, final BigDecimal maxAllowedLienLimit, final boolean lienAllowed, - final BigDecimal nominalAnnualInterestRateOverdraft, final BigDecimal minOverdraftForInterestCalculation, - final boolean withHoldTax) { - - final SavingsAccountStatusType status = SavingsAccountStatusType.SUBMITTED_AND_PENDING_APPROVAL; - return new SavingsAccount(client, group, product, fieldOfficer, accountNo, externalId, status, accountType, submittedOnDate, - submittedBy, interestRate, interestCompoundingPeriodType, interestPostingPeriodType, interestCalculationType, - interestCalculationDaysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType, - withdrawalFeeApplicableForTransfer, savingsAccountCharges, allowOverdraft, overdraftLimit, enforceMinRequiredBalance, - minRequiredBalance, maxAllowedLienLimit, lienAllowed, nominalAnnualInterestRateOverdraft, - minOverdraftForInterestCalculation, withHoldTax); - } - - protected SavingsAccount(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 boolean allowOverdraft, final BigDecimal overdraftLimit, boolean withHoldTax) { - this(client, group, product, fieldOfficer, accountNo, externalId, status, accountType, submittedOnDate, submittedBy, - nominalAnnualInterestRate, interestCompoundingPeriodType, interestPostingPeriodType, interestCalculationType, - interestCalculationDaysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType, - withdrawalFeeApplicableForTransfer, savingsAccountCharges, allowOverdraft, overdraftLimit, false, null, null, false, null, - null, withHoldTax); - } - - protected SavingsAccount(final Client client, final Group group, final SavingsProduct product, final Staff savingsOfficer, - 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 boolean allowOverdraft, final BigDecimal overdraftLimit, final boolean enforceMinRequiredBalance, - final BigDecimal minRequiredBalance, final BigDecimal maxAllowedLienLimit, final boolean lienAllowed, - final BigDecimal nominalAnnualInterestRateOverdraft, final BigDecimal minOverdraftForInterestCalculation, boolean withHoldTax) { - this.client = client; - this.group = group; - this.product = product; - this.savingsOfficer = savingsOfficer; - if (StringUtils.isBlank(accountNo)) { - this.accountNumber = new RandomPasswordGenerator(19).generate(); - this.accountNumberRequiresAutoGeneration = true; - } else { - this.accountNumber = accountNo; - } - - this.currency = product.currency(); - this.externalId = externalId; - this.status = status.getValue(); - this.accountType = accountType.getValue(); - this.submittedOnDate = submittedOnDate; - this.submittedBy = submittedBy; - this.nominalAnnualInterestRate = nominalAnnualInterestRate; - this.interestCompoundingPeriodType = interestCompoundingPeriodType.getValue(); - this.interestPostingPeriodType = interestPostingPeriodType.getValue(); - this.interestCalculationType = interestCalculationType.getValue(); - this.interestCalculationDaysInYearType = interestCalculationDaysInYearType.getValue(); - this.minRequiredOpeningBalance = minRequiredOpeningBalance; - this.lockinPeriodFrequency = lockinPeriodFrequency; - if (lockinPeriodFrequencyType != null) { - this.lockinPeriodFrequencyType = lockinPeriodFrequencyType.getValue(); - } - this.withdrawalFeeApplicableForTransfer = withdrawalFeeApplicableForTransfer; - - if (!CollectionUtils.isEmpty(savingsAccountCharges)) { - this.charges = associateChargesWithThisSavingsAccount(savingsAccountCharges); - } - - this.summary = new SavingsAccountSummary(); - this.allowOverdraft = allowOverdraft; - this.overdraftLimit = overdraftLimit; - this.nominalAnnualInterestRateOverdraft = nominalAnnualInterestRateOverdraft; - this.minOverdraftForInterestCalculation = minOverdraftForInterestCalculation; - esnureOverdraftLimitsSetForOverdraftAccounts(); - - this.enforceMinRequiredBalance = enforceMinRequiredBalance; - this.minRequiredBalance = minRequiredBalance; - this.lienAllowed = lienAllowed; - this.maxAllowedLienLimit = maxAllowedLienLimit; - this.minBalanceForInterestCalculation = product.minBalanceForInterestCalculation(); - // this.savingsOfficerHistory = null; - this.withHoldTax = withHoldTax; - this.taxGroup = product.getTaxGroup(); - } - - /** - * Used after fetching/hydrating a {@link SavingsAccount} object to inject helper services/components used for - * update summary details after events/transactions on a {@link SavingsAccount}. - */ - public void setHelpers(final SavingsAccountTransactionSummaryWrapper savingsAccountTransactionSummaryWrapper, - final SavingsHelper savingsHelper) { - this.savingsAccountTransactionSummaryWrapper = savingsAccountTransactionSummaryWrapper; - this.savingsHelper = savingsHelper; - } - - public void setSavingsAccountTransactions(final List savingsAccountTransactions) { - this.savingsAccountTransactions.addAll(savingsAccountTransactions); - } - - public List getSavingsAccountTransactionsWithPivotConfig() { - return this.savingsAccountTransactions; - } - - public ExternalId getExternalId() { - return externalId; - } - - public boolean isNotActive() { - return !isActive(); - } - - public boolean isActive() { - return SavingsAccountStatusType.fromInt(this.status).isActive(); - } - - public boolean isNotSubmittedAndPendingApproval() { - return !isSubmittedAndPendingApproval(); - } - - public boolean isSubmittedAndPendingApproval() { - return SavingsAccountStatusType.fromInt(this.status).isSubmittedAndPendingApproval(); - } - - public boolean isApproved() { - return SavingsAccountStatusType.fromInt(this.status).isApproved(); - } - - public boolean isActivated() { - boolean isActive = false; - if (this.activatedOnDate != null) { - isActive = true; - } - return isActive; - } - - public boolean isClosed() { - return SavingsAccountStatusType.fromInt(this.status).isClosed(); - } - - public List getIdentifiers() { - return identifiers; - } - - public void postInterest(final MathContext mc, final LocalDate interestPostingUpToDate, final boolean isInterestTransfer, - final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth, - final LocalDate postInterestOnDate, final boolean backdatedTxnsAllowedTill, final boolean postReversals) { - final List postingPeriods = calculateInterestUsing(mc, interestPostingUpToDate, isInterestTransfer, - isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, - postReversals); - if (postingPeriods.isEmpty()) { - return; - } - - Money interestPostedToDate = Money.zero(this.currency); - - if (backdatedTxnsAllowedTill) { - interestPostedToDate = Money.of(this.currency, this.summary.getTotalInterestPosted()); - } - - boolean recalucateDailyBalanceDetails = false; - boolean applyWithHoldTax = isWithHoldTaxApplicableForInterestPosting(); - final List withholdTransactions = new ArrayList<>(); - - if (backdatedTxnsAllowedTill) { - withholdTransactions.addAll(findWithHoldSavingsTransactionsWithPivotConfig()); - } else { - withholdTransactions.addAll(findWithHoldTransactions()); - } - - for (final PostingPeriod interestPostingPeriod : postingPeriods) { - final LocalDate interestPostingTransactionDate = interestPostingPeriod.dateOfPostingTransaction(); - final Money interestEarnedToBePostedForPeriod = interestPostingPeriod.getInterestEarned(); - - if (!DateUtils.isAfter(interestPostingTransactionDate, interestPostingUpToDate)) { - interestPostedToDate = interestPostedToDate.plus(interestEarnedToBePostedForPeriod); - - SavingsAccountTransaction postingTransaction = null; - if (backdatedTxnsAllowedTill) { - postingTransaction = findInterestPostingSavingsTransactionWithPivotConfig(interestPostingTransactionDate); - } else { - postingTransaction = findInterestPostingTransactionFor(interestPostingTransactionDate); - } - if (postingTransaction == null) { - SavingsAccountTransaction newPostingTransaction; - if (interestEarnedToBePostedForPeriod.isGreaterThanOrEqualTo(Money.zero(currency))) { - - newPostingTransaction = SavingsAccountTransaction.interestPosting(this, office(), interestPostingTransactionDate, - interestEarnedToBePostedForPeriod, interestPostingPeriod.isUserPosting()); - } else { - newPostingTransaction = SavingsAccountTransaction.overdraftInterest(this, office(), interestPostingTransactionDate, - interestEarnedToBePostedForPeriod.negated(), interestPostingPeriod.isUserPosting()); - } - if (backdatedTxnsAllowedTill) { - addTransactionToExisting(newPostingTransaction); - } else { - addTransaction(newPostingTransaction); - } - if (applyWithHoldTax) { - createWithHoldTransaction(interestEarnedToBePostedForPeriod.getAmount(), interestPostingTransactionDate, - backdatedTxnsAllowedTill); - } - recalucateDailyBalanceDetails = true; - } else { - boolean correctionRequired = false; - if (postingTransaction.isInterestPostingAndNotReversed()) { - correctionRequired = postingTransaction.hasNotAmount(interestEarnedToBePostedForPeriod); - } else { - correctionRequired = postingTransaction.hasNotAmount(interestEarnedToBePostedForPeriod.negated()); - } - if (correctionRequired) { - boolean applyWithHoldTaxForOldTransaction = false; - postingTransaction.reverse(); - SavingsAccountTransaction reversal = null; - if (postReversals) { - reversal = SavingsAccountTransaction.reversal(postingTransaction); - } - final SavingsAccountTransaction withholdTransaction = findTransactionFor(interestPostingTransactionDate, - withholdTransactions); - if (withholdTransaction != null) { - withholdTransaction.reverse(); - applyWithHoldTaxForOldTransaction = true; - } - SavingsAccountTransaction newPostingTransaction; - if (interestEarnedToBePostedForPeriod.isGreaterThanOrEqualTo(Money.zero(currency))) { - newPostingTransaction = SavingsAccountTransaction.interestPosting(this, office(), - interestPostingTransactionDate, interestEarnedToBePostedForPeriod, - interestPostingPeriod.isUserPosting()); - } else { - newPostingTransaction = SavingsAccountTransaction.overdraftInterest(this, office(), - interestPostingTransactionDate, interestEarnedToBePostedForPeriod.negated(), - interestPostingPeriod.isUserPosting()); - } - if (backdatedTxnsAllowedTill) { - addTransactionToExisting(newPostingTransaction); - if (reversal != null) { - addTransactionToExisting(reversal); - } - } else { - addTransaction(newPostingTransaction); - if (reversal != null) { - addTransaction(reversal); - } - } - if (applyWithHoldTaxForOldTransaction) { - createWithHoldTransaction(interestEarnedToBePostedForPeriod.getAmount(), interestPostingTransactionDate, - backdatedTxnsAllowedTill); - } - recalucateDailyBalanceDetails = true; - } - } - } - } - - if (recalucateDailyBalanceDetails) { - // no openingBalance concept supported yet but probably will to - // allow - // for migrations. - Money openingAccountBalance = Money.zero(this.currency); - - if (backdatedTxnsAllowedTill) { - if (this.summary.getLastInterestCalculationDate() == null) { - openingAccountBalance = Money.zero(this.currency); - } else { - openingAccountBalance = Money.of(this.currency, this.summary.getRunningBalanceOnPivotDate()); - } - } - - // update existing transactions so derived balance fields are - // correct. - recalculateDailyBalances(openingAccountBalance, interestPostingUpToDate, backdatedTxnsAllowedTill, postReversals); - } - - if (!backdatedTxnsAllowedTill) { - this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, this.transactions); - } else { - this.summary.updateSummaryWithPivotConfig(this.currency, this.savingsAccountTransactionSummaryWrapper, null, - this.savingsAccountTransactions); - } - } - - protected List findWithHoldTransactions() { - final List withholdTransactions = new ArrayList<>(); - List trans = getTransactions(); - for (final SavingsAccountTransaction transaction : trans) { - if (transaction.isWithHoldTaxAndNotReversed()) { - withholdTransactions.add(transaction); - } - } - return withholdTransactions; - } - - protected List findWithHoldSavingsTransactionsWithPivotConfig() { - final List withholdTransactions = new ArrayList<>(); - List trans = getSavingsAccountTransactionsWithPivotConfig(); - for (final SavingsAccountTransaction transaction : trans) { - if (transaction.isWithHoldTaxAndNotReversed() && !transaction.isReversalTransaction()) { - withholdTransactions.add(transaction); - } - } - return withholdTransactions; - } - - private boolean isWithHoldTaxApplicableForInterestPosting() { - return this.withHoldTax() && this.depositAccountType().isSavingsDeposit(); - } - - protected SavingsAccountTransaction findInterestPostingTransactionFor(final LocalDate postingDate) { - SavingsAccountTransaction postingTransation = null; - List trans = getTransactions(); - for (final SavingsAccountTransaction transaction : trans) { - if ((transaction.isInterestPostingAndNotReversed() || transaction.isOverdraftInterestAndNotReversed()) - && transaction.occursOn(postingDate) && !transaction.isReversalTransaction()) { - postingTransation = transaction; - break; - } - } - return postingTransation; - } - - protected SavingsAccountTransaction findInterestPostingSavingsTransactionWithPivotConfig(final LocalDate postingDate) { - SavingsAccountTransaction postingTransation = null; - List trans = getSavingsAccountTransactionsWithPivotConfig(); - for (final SavingsAccountTransaction transaction : trans) { - if ((transaction.isInterestPostingAndNotReversed() || transaction.isOverdraftInterestAndNotReversed()) - && transaction.occursOn(postingDate) && !transaction.isReversalTransaction()) { - postingTransation = transaction; - break; - } - } - return postingTransation; - } - - protected SavingsAccountTransaction findTransactionFor(final LocalDate postingDate, - final List transactions) { - SavingsAccountTransaction transaction = null; - for (final SavingsAccountTransaction savingsAccountTransaction : transactions) { - if (savingsAccountTransaction.occursOn(postingDate)) { - transaction = savingsAccountTransaction; - break; - } - } - return transaction; - } - - protected boolean createWithHoldTransaction(final BigDecimal amount, final LocalDate date, final boolean backdatedTxnsAllowedTill) { - boolean isTaxAdded = false; - if (this.taxGroup != null && amount.compareTo(BigDecimal.ZERO) > 0) { - Map taxSplit = TaxUtils.splitTax(amount, date, this.taxGroup.getTaxGroupMappings(), amount.scale()); - BigDecimal totalTax = TaxUtils.totalTaxAmount(taxSplit); - if (totalTax.compareTo(BigDecimal.ZERO) > 0) { - SavingsAccountTransaction withholdTransaction = SavingsAccountTransaction.withHoldTax(this, office(), date, - Money.of(currency, totalTax), taxSplit); - if (backdatedTxnsAllowedTill) { - addTransactionToExisting(withholdTransaction); - } else { - addTransaction(withholdTransaction); - } - isTaxAdded = true; - } - } - return isTaxAdded; - } - - protected boolean updateWithHoldTransaction(final BigDecimal amount, final SavingsAccountTransaction withholdTransaction) { - boolean isTaxAdded = false; - if (this.taxGroup != null && amount.compareTo(BigDecimal.ZERO) > 0) { - Map taxSplit = TaxUtils.splitTax(amount, withholdTransaction.getTransactionDate(), - this.taxGroup.getTaxGroupMappings(), amount.scale()); - BigDecimal totalTax = TaxUtils.totalTaxAmount(taxSplit); - if (totalTax.compareTo(BigDecimal.ZERO) > 0) { - if (withholdTransaction.getId() == null) { - withholdTransaction.setAmount(Money.of(currency, totalTax)); - withholdTransaction.getTaxDetails().clear(); - SavingsAccountTransaction.updateTaxDetails(taxSplit, withholdTransaction); - isTaxAdded = true; - } else if (totalTax.compareTo(withholdTransaction.getAmount()) != 0) { - withholdTransaction.reverse(); - SavingsAccountTransaction newWithholdTransaction = SavingsAccountTransaction.withHoldTax(this, office(), - withholdTransaction.getTransactionDate(), Money.of(currency, totalTax), taxSplit); - addTransaction(newWithholdTransaction); - isTaxAdded = true; - } - } - } - return isTaxAdded; - } - - // Determine the last transaction for given day - protected SavingsAccountTransaction findLastTransaction(final LocalDate date) { - - SavingsAccountTransaction savingsTransaction = null; - List trans = getTransactions(); - for (final SavingsAccountTransaction transaction : trans) { - if (transaction.isNotReversed() && !transaction.isReversalTransaction() && transaction.occursOn(date)) { - savingsTransaction = transaction; - break; - } - } - - return savingsTransaction; - } - - protected SavingsAccountTransaction findLastFilteredTransactionWithPivotConfig(final LocalDate date) { - SavingsAccountTransaction savingsTransaction = null; - List trans = getSavingsAccountTransactionsWithPivotConfig(); - for (final SavingsAccountTransaction transaction : trans) { - if (transaction.isNotReversed() && !transaction.isReversalTransaction() && transaction.occursOn(date)) { - savingsTransaction = transaction; - break; - } - } - return savingsTransaction; - } - - public List getManualPostingDates() { - List transactions = new ArrayList<>(); - for (SavingsAccountTransaction trans : this.transactions) { - if (trans.isInterestPosting() && trans.isNotReversed() && !trans.isReversalTransaction() && trans.isManualTransaction()) { - transactions.add(trans.getTransactionDate()); - } - } - return transactions; - } - - public List getManualPostingDatesWithPivotConfig() { - List transactions = new ArrayList<>(); - for (SavingsAccountTransaction trans : this.savingsAccountTransactions) { - if (trans.isInterestPosting() && trans.isNotReversed() && trans.isManualTransaction()) { - transactions.add(trans.getTransactionDate()); - } - } - return transactions; - } - - /** - * All interest calculation based on END-OF-DAY-BALANCE. - * - * Interest calculation is performed on-the-fly over all account transactions. - * - * - * 1. Calculate Interest From Beginning Of Account 1a. determine the 'crediting' periods that exist for this savings - * acccount 1b. determine the 'compounding' periods that exist within each 'crediting' period calculate the amount - * of interest due at the end of each 'crediting' period check if an existing 'interest posting' transaction exists - * for date and matches the amount posted - * - * @param isInterestTransfer - * TODO - */ - - public List calculateInterestUsing(final MathContext mc, final LocalDate upToInterestCalculationDate, - boolean isInterestTransfer, final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth, - final LocalDate postInterestOnDate, final boolean backdatedTxnsAllowedTill, final boolean postReversals) { - - // no openingBalance concept supported yet but probably will to allow for migrations. - // Check global configurations and 'pivot' date is null - Money openingAccountBalance = backdatedTxnsAllowedTill ? Money.of(this.currency, this.summary.getRunningBalanceOnPivotDate()) - : Money.zero(this.currency); - - // update existing transactions so derived balance fields are correct. - recalculateDailyBalances(openingAccountBalance, upToInterestCalculationDate, backdatedTxnsAllowedTill, postReversals); - - final List allPostingPeriods = new ArrayList<>(); - if (hasInterestCalculation() || hasOverdraftInterestCalculation()) { - // 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 = null; - if (backdatedTxnsAllowedTill) { - postedAsOnDates = getManualPostingDatesWithPivotConfig(); - } else { - postedAsOnDates = getManualPostingDates(); - } - if (postInterestOnDate != null) { - postedAsOnDates.add(postInterestOnDate); - } - final List postingPeriodIntervals = this.savingsHelper.determineInterestPostingPeriods( - getStartInterestCalculationDate(), upToInterestCalculationDate, postingPeriodType, financialYearBeginningMonth, - postedAsOnDates); - - Money periodStartingBalance; - if (this.startInterestCalculationDate != null && !this.getStartInterestCalculationDate().equals(this.getActivationDate())) { - LocalDate startInterestCalculationDate = this.startInterestCalculationDate; - SavingsAccountTransaction transaction = null; - if (backdatedTxnsAllowedTill) { - transaction = findLastFilteredTransactionWithPivotConfig(startInterestCalculationDate); - } else { - transaction = findLastTransaction(startInterestCalculationDate); - } - - if (transaction == null) { - periodStartingBalance = Money.zero(this.currency); - } else { - periodStartingBalance = Money.of(this.currency, this.summary.getRunningBalanceOnPivotDate()); - } - } else { - periodStartingBalance = Money.zero(this.currency); - } - - final SavingsInterestCalculationType interestCalculationType = SavingsInterestCalculationType - .fromInt(this.interestCalculationType); - final BigDecimal interestRateAsFraction = getEffectiveInterestRateAsFraction(mc, upToInterestCalculationDate); - final BigDecimal overdraftInterestRateAsFraction = getEffectiveOverdraftInterestRateAsFraction(mc); - final Collection interestPostTransactions = this.savingsHelper.fetchPostInterestTransactionIds(getId()); - final Money minBalanceForInterestCalculation = Money.of(getCurrency(), minBalanceForInterestCalculation()); - final Money minOverdraftForInterestCalculation = Money.of(getCurrency(), this.minOverdraftForInterestCalculation); - - for (final LocalDateInterval periodInterval : postingPeriodIntervals) { - - boolean isUserPosting = false; - if (postedAsOnDates.contains(periodInterval.endDate().plusDays(1))) { - isUserPosting = true; - } - - PostingPeriod postingPeriod = null; - List orderedNonInterestPostingTransactions = null; - if (backdatedTxnsAllowedTill) { - orderedNonInterestPostingTransactions = retreiveOrderedNonInterestPostingSavingsTransactionsWithPivotConfig(); - } else { - orderedNonInterestPostingTransactions = retreiveOrderedNonInterestPostingTransactions(); - } - - List savingsAccountTransactionDetailsForPostingPeriod = toSavingsAccountTransactionDetailsForPostingPeriodList( - orderedNonInterestPostingTransactions); - - postingPeriod = PostingPeriod.createFrom(periodInterval, periodStartingBalance, - savingsAccountTransactionDetailsForPostingPeriod, this.currency, compoundingPeriodType, interestCalculationType, - interestRateAsFraction, daysInYearType.getValue(), upToInterestCalculationDate, interestPostTransactions, - isInterestTransfer, minBalanceForInterestCalculation, isSavingsInterestPostingAtCurrentPeriodEnd, - overdraftInterestRateAsFraction, minOverdraftForInterestCalculation, isUserPosting, financialYearBeginningMonth); - - periodStartingBalance = postingPeriod.closingBalance(); - - allPostingPeriods.add(postingPeriod); - } - - this.savingsHelper.calculateInterestForAllPostingPeriods(this.currency, allPostingPeriods, getLockedInUntilDate(), - isTransferInterestToOtherAccount()); - - this.summary.updateFromInterestPeriodSummaries(this.currency, allPostingPeriods); - } - - if (backdatedTxnsAllowedTill) { - this.summary.updateSummaryWithPivotConfig(this.currency, this.savingsAccountTransactionSummaryWrapper, null, - this.savingsAccountTransactions); - } else { - this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, this.transactions); - } - - return allPostingPeriods; - } - - private BigDecimal getEffectiveOverdraftInterestRateAsFraction(MathContext mc) { - return this.nominalAnnualInterestRateOverdraft.divide(BigDecimal.valueOf(100L), mc); - } - - public BigDecimal getEffectiveInterestRateAsFractionAccrual(final MathContext mc, final LocalDate upToInterestCalculationDate) { - return this.nominalAnnualInterestRate.divide(BigDecimal.valueOf(100L), mc); - } - - @SuppressWarnings("unused") - protected BigDecimal getEffectiveInterestRateAsFraction(final MathContext mc, final LocalDate upToInterestCalculationDate) { - return this.nominalAnnualInterestRate.divide(BigDecimal.valueOf(100L), mc); - } - - private boolean hasInterestCalculation() { - return !MathUtil.isEmpty(nominalAnnualInterestRate); - } - - private boolean hasOverdraftInterestCalculation() { - return isAllowOverdraft() && !MathUtil.isEmpty(getOverdraftLimit()) && !MathUtil.isEmpty(nominalAnnualInterestRateOverdraft); - } - - public List retrieveOrderedAccrualTransactions() { - return retrieveListOfTransactions().stream().filter(SavingsAccountTransaction::isAccrual) - .sorted(new SavingsAccountTransactionComparator()).collect(Collectors.toList()); - } - - protected List retreiveOrderedNonInterestPostingTransactions() { - final List listOfTransactionsSorted = retrieveListOfTransactions(); - - final List orderedNonInterestPostingTransactions = new ArrayList<>(); - - for (final SavingsAccountTransaction transaction : listOfTransactionsSorted) { - if (!(transaction.isInterestPostingAndNotReversed() || transaction.isOverdraftInterestAndNotReversed()) - && transaction.isNotReversed() && !transaction.isReversalTransaction()) { - orderedNonInterestPostingTransactions.add(transaction); - } - } - orderedNonInterestPostingTransactions.sort(new SavingsAccountTransactionComparator()); - return orderedNonInterestPostingTransactions; - } - - protected List retreiveOrderedNonInterestPostingSavingsTransactionsWithPivotConfig() { - final List listOfTransactionsSorted = retrieveSortedTransactions(); - - final List orderedNonInterestPostingTransactions = new ArrayList<>(); - - for (final SavingsAccountTransaction transaction : listOfTransactionsSorted) { - if (!(transaction.isInterestPostingAndNotReversed() || transaction.isOverdraftInterestAndNotReversed()) - && transaction.isNotReversed() && !transaction.isReversalTransaction()) { - orderedNonInterestPostingTransactions.add(transaction); - } - } - orderedNonInterestPostingTransactions.sort(new SavingsAccountTransactionComparator()); - return orderedNonInterestPostingTransactions; - } - - protected List retrieveSortedTransactions() { - final List listOfTransactionsSorted = new ArrayList<>(); - listOfTransactionsSorted.addAll(this.savingsAccountTransactions); - final SavingsAccountTransactionComparator transactionComparator = new SavingsAccountTransactionComparator(); - Collections.sort(listOfTransactionsSorted, transactionComparator); - return listOfTransactionsSorted; - } - - protected List retrieveListOfTransactions() { - final List listOfTransactionsSorted = new ArrayList<>(); - listOfTransactionsSorted.addAll(this.transactions); - - final SavingsAccountTransactionComparator transactionComparator = new SavingsAccountTransactionComparator(); - Collections.sort(listOfTransactionsSorted, transactionComparator); - return listOfTransactionsSorted; - } - - protected void recalculateDailyBalances(final Money openingAccountBalance, final LocalDate interestPostingUpToDate, - final boolean backdatedTxnsAllowedTill, boolean postReversals) { - Money runningBalance = openingAccountBalance; - boolean calculateInterest = hasInterestCalculation() || hasOverdraftInterestCalculation(); - - List accountTransactionsSorted = null; - - if (backdatedTxnsAllowedTill) { - accountTransactionsSorted = retrieveSortedTransactions(); - } else { - accountTransactionsSorted = retrieveListOfTransactions(); - } - - boolean isTransactionsModified = false; - for (final SavingsAccountTransaction transaction : accountTransactionsSorted) { - if (transaction.isReversed() || transaction.isReversalTransaction()) { - transaction.zeroBalanceFields(); - } else { - Money overdraftAmount = Money.zero(this.currency); - Money transactionAmount = Money.zero(this.currency); - if ((transaction.isCredit() || transaction.isAmountRelease())) { - if (runningBalance.isLessThanZero()) { - Money diffAmount = transaction.getAmount(this.currency).plus(runningBalance); - if (diffAmount.isGreaterThanZero()) { - overdraftAmount = transaction.getAmount(this.currency).minus(diffAmount); - } else { - overdraftAmount = transaction.getAmount(this.currency); - } - } - transactionAmount = transactionAmount.plus(transaction.getAmount(this.currency)); - } else if (transaction.isDebit() || transaction.isAmountOnHold()) { - if (runningBalance.isLessThanZero()) { - overdraftAmount = transaction.getAmount(this.currency); - } - transactionAmount = transactionAmount.minus(transaction.getAmount(this.currency)); - } - - runningBalance = runningBalance.plus(transactionAmount); - transaction.setRunningBalance(runningBalance); - - if (MathUtil.isEmpty(overdraftAmount) && runningBalance.isLessThanZero() && !transaction.isAmountOnHold()) { - overdraftAmount = runningBalance.negated(); - } - if (!calculateInterest || transaction.getId() == null || transaction.getOverdraftAmount(this.currency).isZero()) { - transaction.setOverdraftAmount(overdraftAmount); - } else if (!MathUtil.isEqualTo(overdraftAmount, transaction.getOverdraftAmount(this.currency)) - && !transaction.isAccrual()) { - SavingsAccountTransaction accountTransaction = SavingsAccountTransaction.copyTransaction(transaction); - if (transaction.isChargeTransaction()) { - Set chargesPaidBy = transaction.getSavingsAccountChargesPaid(); - final Set newChargePaidBy = new HashSet<>(); - chargesPaidBy.forEach(x -> newChargePaidBy - .add(SavingsAccountChargePaidBy.instance(accountTransaction, x.getSavingsAccountCharge(), x.getAmount()))); - accountTransaction.getSavingsAccountChargesPaid().addAll(newChargePaidBy); - } - SavingsAccountTransaction reversal = null; - transaction.reverse(); - if (postReversals) { - reversal = SavingsAccountTransaction.reversal(transaction); - } - if (MathUtil.isGreaterThanZero(overdraftAmount)) { - accountTransaction.setOverdraftAmount(overdraftAmount); - } - accountTransaction.setRunningBalance(runningBalance); - if (backdatedTxnsAllowedTill) { - addTransactionToExisting(accountTransaction); - if (reversal != null) { - addTransactionToExisting(reversal); - } - } else { - addTransaction(accountTransaction); - if (reversal != null) { - addTransaction(reversal); - } - } - isTransactionsModified = true; - } - - } - } - - if (isTransactionsModified) { - if (backdatedTxnsAllowedTill) { - accountTransactionsSorted = retrieveSortedTransactions(); - } else { - accountTransactionsSorted = retrieveListOfTransactions(); - } - } - resetAccountTransactionsEndOfDayBalances(accountTransactionsSorted, interestPostingUpToDate); - } - - protected void resetAccountTransactionsEndOfDayBalances(final List accountTransactionsSorted, - final LocalDate interestPostingUpToDate) { - // loop over transactions in reverse - LocalDate endOfBalanceDate = interestPostingUpToDate; - for (int i = accountTransactionsSorted.size() - 1; i >= 0; i--) { - final SavingsAccountTransaction transaction = accountTransactionsSorted.get(i); - if (transaction.isNotReversed() && !transaction.isReversalTransaction() - && !(transaction.isInterestPostingAndNotReversed() || transaction.isOverdraftInterestAndNotReversed())) { - transaction.updateCumulativeBalanceAndDates(this.currency, endOfBalanceDate); - // this transactions transaction date is end of balance date for - // previous transaction. - endOfBalanceDate = transaction.getTransactionDate().minusDays(1); - } - } - } - - public SavingsAccountTransaction deposit(final SavingsAccountTransactionDTO transactionDTO, final boolean backdatedTxnsAllowedTill, - final Long relaxingDaysConfigForPivotDate, final String refNo) { - return deposit(transactionDTO, SavingsAccountTransactionType.DEPOSIT, backdatedTxnsAllowedTill, relaxingDaysConfigForPivotDate, - refNo); - } - - public SavingsAccountTransaction dividendPayout(final SavingsAccountTransactionDTO transactionDTO, - final boolean backdatedTxnsAllowedTill, final Long relaxingDaysConfigForPivotDate) { - String refNo = null; - return deposit(transactionDTO, SavingsAccountTransactionType.DIVIDEND_PAYOUT, backdatedTxnsAllowedTill, - relaxingDaysConfigForPivotDate, refNo); - } - - public SavingsAccountTransaction deposit(final SavingsAccountTransactionDTO transactionDTO, - final SavingsAccountTransactionType savingsAccountTransactionType, final boolean backdatedTxnsAllowedTill, - final Long relaxingDaysConfigForPivotDate, final String refNo) { - final String resourceTypeName = depositAccountType().resourceName(); - if (isNotActive()) { - final String defaultUserMessage = "Transaction is not allowed. Account is not active."; - final ApiParameterError error = ApiParameterError.parameterError( - "error.msg." + resourceTypeName + ".transaction.account.is.not.active", defaultUserMessage, "transactionDate", - transactionDTO.getTransactionDate().format(transactionDTO.getFormatter())); - - final List dataValidationErrors = new ArrayList<>(); - dataValidationErrors.add(error); - - throw new PlatformApiDataValidationException(dataValidationErrors); - } - - if (DateUtils.isDateInTheFuture(transactionDTO.getTransactionDate())) { - final String defaultUserMessage = "Transaction date cannot be in the future."; - final ApiParameterError error = ApiParameterError.parameterError("error.msg." + resourceTypeName + ".transaction.in.the.future", - defaultUserMessage, "transactionDate", transactionDTO.getTransactionDate().format(transactionDTO.getFormatter())); - - final List dataValidationErrors = new ArrayList<>(); - dataValidationErrors.add(error); - - throw new PlatformApiDataValidationException(dataValidationErrors); - } - - if (DateUtils.isBefore(transactionDTO.getTransactionDate(), getActivationDate())) { - final Object[] defaultUserArgs = Arrays.asList(transactionDTO.getTransactionDate().format(transactionDTO.getFormatter()), - getActivationDate().format(transactionDTO.getFormatter())).toArray(); - final String defaultUserMessage = "Transaction date cannot be before accounts activation date."; - final ApiParameterError error = ApiParameterError.parameterError( - "error.msg." + resourceTypeName + ".transaction.before.activation.date", defaultUserMessage, "transactionDate", - defaultUserArgs); - - final List dataValidationErrors = new ArrayList<>(); - dataValidationErrors.add(error); - - throw new PlatformApiDataValidationException(dataValidationErrors); - } - validatePivotDateTransaction(transactionDTO.getTransactionDate(), backdatedTxnsAllowedTill, relaxingDaysConfigForPivotDate, - resourceTypeName); - - validateActivityNotBeforeClientOrGroupTransferDate(SavingsEvent.SAVINGS_DEPOSIT, transactionDTO.getTransactionDate()); - - final Money amount = Money.of(this.currency, transactionDTO.getTransactionAmount()); - - final SavingsAccountTransaction transaction = SavingsAccountTransaction.deposit(this, office(), transactionDTO.getPaymentDetail(), - transactionDTO.getTransactionDate(), amount, savingsAccountTransactionType, refNo); - - if (backdatedTxnsAllowedTill) { - addTransactionToExisting(transaction); - } else { - this.accrualsForSavingsReverse(transactionDTO, backdatedTxnsAllowedTill); - addTransaction(transaction); - } - - if (this.sub_status.equals(SavingsAccountSubStatusEnum.INACTIVE.getValue()) - || this.sub_status.equals(SavingsAccountSubStatusEnum.DORMANT.getValue())) { - this.sub_status = SavingsAccountSubStatusEnum.NONE.getValue(); - } - - if (backdatedTxnsAllowedTill) { - this.summary.updateSummaryWithPivotConfig(this.currency, this.savingsAccountTransactionSummaryWrapper, transaction, - this.savingsAccountTransactions); - } - - return transaction; - } - - public void validatePivotDateTransaction(LocalDate transactionDate, final boolean backdatedTxnsAllowedTill, - final Long relaxingDaysConfigForPivotDate, final String resourceTypeName) { - if (backdatedTxnsAllowedTill) { - if (this.getSummary().getInterestPostedTillDate() != null && DateUtils.isBefore(transactionDate, - getSummary().getInterestPostedTillDate().minusDays(relaxingDaysConfigForPivotDate))) { - final Object[] defaultUserArgs = Arrays.asList(transactionDate, getActivationDate()).toArray(); - final String defaultUserMessage = "Transaction date cannot be before transactions pivot date."; - final ApiParameterError error = ApiParameterError.parameterError( - "error.msg." + resourceTypeName + ".transaction.before.pivot.date", defaultUserMessage, "transactionDate", - defaultUserArgs); - - final List dataValidationErrors = new ArrayList<>(); - dataValidationErrors.add(error); - - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - } - - public LocalDate getActivationDate() { - return this.activatedOnDate; - } - - public AppUser getActivatedBy() { - return this.activatedBy; - } - - public LocalDate getWithdrawnOnDate() { - return this.withdrawnOnDate; - } - - public AppUser getWithdrawnBy() { - return this.withdrawnBy; - } - - // startInterestCalculationDate is set during migration so that there is no - // interference with interest posting of previous system - public LocalDate getStartInterestCalculationDate() { - LocalDate startInterestCalculationLocalDate = null; - if (this.startInterestCalculationDate != null) { - startInterestCalculationLocalDate = this.startInterestCalculationDate; - } else { - startInterestCalculationLocalDate = getActivationDate(); - } - return startInterestCalculationLocalDate; - } - - public void setStartInterestCalculationDate(LocalDate startInterestCalculationDate) { - this.startInterestCalculationDate = startInterestCalculationDate; - } - - public SavingsAccountTransaction withdraw(final SavingsAccountTransactionDTO transactionDTO, final boolean applyWithdrawFee, - final boolean backdatedTxnsAllowedTill, final Long relaxingDaysConfigForPivotDate, String refNo) { - if (!isTransactionsAllowed()) { - - final String defaultUserMessage = "Transaction is not allowed. Account is not active."; - final ApiParameterError error = ApiParameterError.parameterError("error.msg.savingsaccount.transaction.account.is.not.active", - defaultUserMessage, "transactionDate", transactionDTO.getTransactionDate().format(transactionDTO.getFormatter())); - - final List dataValidationErrors = new ArrayList<>(); - dataValidationErrors.add(error); - - throw new PlatformApiDataValidationException(dataValidationErrors); - } - - if (DateUtils.isDateInTheFuture(transactionDTO.getTransactionDate())) { - final String defaultUserMessage = "Transaction date cannot be in the future."; - final ApiParameterError error = ApiParameterError.parameterError("error.msg.savingsaccount.transaction.in.the.future", - defaultUserMessage, "transactionDate", transactionDTO.getTransactionDate().format(transactionDTO.getFormatter())); - - final List dataValidationErrors = new ArrayList<>(); - dataValidationErrors.add(error); - - throw new PlatformApiDataValidationException(dataValidationErrors); - } - - if (DateUtils.isBefore(transactionDTO.getTransactionDate(), getActivationDate())) { - final Object[] defaultUserArgs = Arrays.asList(transactionDTO.getTransactionDate().format(transactionDTO.getFormatter()), - getActivationDate().format(transactionDTO.getFormatter())).toArray(); - final String defaultUserMessage = "Transaction date cannot be before accounts activation date."; - final ApiParameterError error = ApiParameterError.parameterError("error.msg.savingsaccount.transaction.before.activation.date", - defaultUserMessage, "transactionDate", defaultUserArgs); - - final List dataValidationErrors = new ArrayList<>(); - dataValidationErrors.add(error); - - throw new PlatformApiDataValidationException(dataValidationErrors); - } - - if (isAccountLocked(transactionDTO.getTransactionDate())) { - final String defaultUserMessage = "Withdrawal is not allowed. No withdrawals are allowed until after " - + getLockedInUntilDate().format(transactionDTO.getFormatter()); - final ApiParameterError error = ApiParameterError.parameterError( - "error.msg.savingsaccount.transaction.withdrawals.blocked.during.lockin.period", defaultUserMessage, "transactionDate", - transactionDTO.getTransactionDate().format(transactionDTO.getFormatter()), - getLockedInUntilDate().format(transactionDTO.getFormatter())); - - final List dataValidationErrors = new ArrayList<>(); - dataValidationErrors.add(error); - - throw new PlatformApiDataValidationException(dataValidationErrors); - } - validatePivotDateTransaction(transactionDTO.getTransactionDate(), backdatedTxnsAllowedTill, relaxingDaysConfigForPivotDate, - "savingsaccount"); - validateActivityNotBeforeClientOrGroupTransferDate(SavingsEvent.SAVINGS_WITHDRAWAL, transactionDTO.getTransactionDate()); - - if (applyWithdrawFee) { - // auto pay withdrawal fee - payWithdrawalFee(transactionDTO.getTransactionAmount(), transactionDTO.getTransactionDate(), transactionDTO.getPaymentDetail(), - backdatedTxnsAllowedTill, refNo); - } - - final Money transactionAmountMoney = Money.of(this.currency, transactionDTO.getTransactionAmount()); - final SavingsAccountTransaction transaction = SavingsAccountTransaction.withdrawal(this, office(), - transactionDTO.getPaymentDetail(), transactionDTO.getTransactionDate(), transactionAmountMoney, refNo); - - if (backdatedTxnsAllowedTill) { - addTransactionToExisting(transaction); - } else { - this.accrualsForSavingsReverse(transactionDTO, backdatedTxnsAllowedTill); - addTransaction(transaction); - } - - if (this.sub_status.equals(SavingsAccountSubStatusEnum.INACTIVE.getValue()) - || this.sub_status.equals(SavingsAccountSubStatusEnum.DORMANT.getValue())) { - this.sub_status = SavingsAccountSubStatusEnum.NONE.getValue(); - } - if (backdatedTxnsAllowedTill) { - this.summary.updateSummaryWithPivotConfig(this.currency, this.savingsAccountTransactionSummaryWrapper, transaction, - this.savingsAccountTransactions); - } - return transaction; - } - - public BigDecimal calculateWithdrawalFee(final BigDecimal transactionAmount) { - BigDecimal result = BigDecimal.ZERO; - if (isWithdrawalFeeApplicableForTransfer()) { - for (SavingsAccountCharge charge : this.charges()) { - if (charge.isWithdrawalFee() && charge.isActive()) { - result = result.add(charge.calculateWithdralFeeAmount(transactionAmount), MoneyHelper.getMathContext()); - } - } - } - return result; - } - - private void payWithdrawalFee(final BigDecimal transactionAmount, final LocalDate transactionDate, final PaymentDetail paymentDetail, - final boolean backdatedTxnsAllowedTill, final String refNo) { - for (SavingsAccountCharge charge : this.charges()) { - if (charge.isWithdrawalFee() && charge.isActive()) { - - if (charge.getFreeWithdrawalCount() == null) { - charge.setFreeWithdrawalCount(0); - } - - if (charge.isEnablePaymentType() && charge.isEnableFreeWithdrawal()) { // discount transaction to - // specific paymentType - if (paymentDetail != null && paymentDetail.getPaymentType() != null - && paymentDetail.getPaymentType().getName().equals(charge.getCharge().getPaymentType().getName())) { - resetFreeChargeDaysCount(charge, transactionAmount, transactionDate, refNo); - } - } else if (charge.isEnablePaymentType()) { // normal charge-transaction to specific paymentType - if (paymentDetail != null && paymentDetail.getPaymentType() != null - && paymentDetail.getPaymentType().getName().equals(charge.getCharge().getPaymentType().getName())) { - charge.updateWithdralFeeAmount(transactionAmount); - this.payCharge(charge, charge.getAmountOutstanding(this.getCurrency()), transactionDate, backdatedTxnsAllowedTill, - refNo); - } - } else if (!charge.isEnablePaymentType() && charge.isEnableFreeWithdrawal()) { // discount transaction - // irrespective of - // PaymentTypes. - resetFreeChargeDaysCount(charge, transactionAmount, transactionDate, refNo); - - } else { // normal-withdraw - charge.updateWithdralFeeAmount(transactionAmount); - this.payCharge(charge, charge.getAmountOutstanding(this.getCurrency()), transactionDate, backdatedTxnsAllowedTill, - refNo); - } - } - } - } - - private void resetFreeChargeDaysCount(SavingsAccountCharge charge, final BigDecimal transactionAmount, final LocalDate transactionDate, - final String refNo) { - LocalDate resetDate = charge.getResetChargeDate(); - - Integer restartPeriod = charge.getRestartFrequency(); - if (charge.getRestartFrequencyEnum() == 2) { // calculation for months - LocalDate localDate = DateUtils.getBusinessLocalDate(); - - LocalDate resetLocalDate; - if (resetDate == null) { - resetLocalDate = this.activatedOnDate; - } else { - resetLocalDate = resetDate; - } - - LocalDate gapIntervalMonth = resetLocalDate.plusMonths(restartPeriod); - - YearMonth gapYearMonth = YearMonth.from(gapIntervalMonth); - YearMonth localYearMonth = YearMonth.from(localDate); - if (localYearMonth.isBefore(gapYearMonth)) { - countValidation(charge, transactionAmount, transactionDate, refNo); - } else { - discountCharge(1, charge); - } - } else { // calculation for days - long completedDays; - - if (resetDate == null) { - completedDays = DAYS.between(DateUtils.getBusinessLocalDate(), this.activatedOnDate); - - } else { - completedDays = DAYS.between(DateUtils.getBusinessLocalDate(), resetDate); - } - - int totalDays = (int) completedDays; - - if (totalDays < restartPeriod) { - countValidation(charge, transactionAmount, transactionDate, refNo); - } else { - discountCharge(1, charge); - } - } - } - - private void countValidation(SavingsAccountCharge charge, final BigDecimal transactionAmount, final LocalDate transactionDate, - final String refNo) { - boolean backdatedTxnsAllowedTill = false; - if (charge.getFreeWithdrawalCount() < charge.getFrequencyFreeWithdrawalCharge()) { - final Integer count = charge.getFreeWithdrawalCount() + 1; - charge.setFreeWithdrawalCount(count); - charge.updateNoWithdrawalFee(); - } else { - charge.updateWithdralFeeAmount(transactionAmount); - this.payCharge(charge, charge.getAmountOutstanding(this.getCurrency()), transactionDate, backdatedTxnsAllowedTill, refNo); - } - } - - private void discountCharge(Integer freeWithdrawalCount, SavingsAccountCharge charge) { - charge.setFreeWithdrawalCount(freeWithdrawalCount); - charge.setDiscountDueDate(DateUtils.getBusinessLocalDate()); - charge.updateNoWithdrawalFee(); - } - - public boolean isBeforeLastPostingPeriod(final LocalDate transactionDate, final boolean backdatedTxnsAllowedTill) { - - boolean transactionBeforeLastInterestPosting = false; - - if (!backdatedTxnsAllowedTill) { - for (final SavingsAccountTransaction transaction : retrieveListOfTransactions()) { - if ((transaction.isInterestPostingAndNotReversed() || transaction.isOverdraftInterestAndNotReversed()) - && transaction.isAfter(transactionDate) && !transaction.isReversalTransaction()) { - transactionBeforeLastInterestPosting = true; - break; - } - } - } else { - if (this.summary.getInterestPostedTillDate() == null) { - return false; - } - transactionBeforeLastInterestPosting = DateUtils.isBefore(transactionDate, this.summary.getInterestPostedTillDate()); - } - - return transactionBeforeLastInterestPosting; - } - - public void validateAccountBalanceDoesNotBecomeNegative(final BigDecimal transactionAmount, final boolean isException, - final List depositAccountOnHoldTransactions, final boolean backdatedTxnsAllowedTill) { - - List transactionsSortedByDate = null; - - if (backdatedTxnsAllowedTill) { - transactionsSortedByDate = retrieveSortedTransactions(); - } else { - transactionsSortedByDate = retrieveListOfTransactions(); - } - - Money runningBalance = Money.zero(this.currency); - if (backdatedTxnsAllowedTill) { - runningBalance = Money.of(this.currency, this.summary.getRunningBalanceOnPivotDate()); - } - - Money minRequiredBalance = minRequiredBalanceDerived(getCurrency()); - LocalDate lastSavingsDate = null; - final BigDecimal withdrawalFee = null; - for (final SavingsAccountTransaction transaction : transactionsSortedByDate) { - if (transaction.isNotReversed() && transaction.isCredit() && !transaction.isReversalTransaction()) { - runningBalance = runningBalance.plus(transaction.getAmount(this.currency)); - } else if (transaction.isNotReversed() && transaction.isDebit() && !transaction.isReversalTransaction()) { - runningBalance = runningBalance.minus(transaction.getAmount(this.currency)); - } else { - continue; - } - - /* - * Loop through the onHold funds and see if we need to deduct or add to minimum required balance and the - * point in time the transaction was made: - */ - if (depositAccountOnHoldTransactions != null) { - for (final DepositAccountOnHoldTransaction onHoldTransaction : depositAccountOnHoldTransactions) { - // Compare the balance of the on hold: - if (!DateUtils.isAfter(onHoldTransaction.getTransactionDate(), transaction.getTransactionDate()) - && (lastSavingsDate == null || DateUtils.isAfter(onHoldTransaction.getTransactionDate(), lastSavingsDate))) { - if (onHoldTransaction.getTransactionType().isHold()) { - minRequiredBalance = minRequiredBalance.plus(onHoldTransaction.getAmount(this.currency)); - } else { - minRequiredBalance = minRequiredBalance.minus(onHoldTransaction.getAmount(this.currency)); - } - } - } - } - - // deal with potential minRequiredBalance and - // enforceMinRequiredBalance - if (!isException && transaction.canProcessBalanceCheck() && !isOverdraft()) { - if (runningBalance.minus(minRequiredBalance).isLessThanZero()) { - throw new InsufficientAccountBalanceException("transactionAmount", getAccountBalance(), withdrawalFee, - transactionAmount); - } - } - lastSavingsDate = transaction.getTransactionDate(); - - } - - // In overdraft cases, minRequiredBalance can be in violation after - // interest posting - // and should be checked after processing all transactions - if (isOverdraft()) { - if (runningBalance.minus(minRequiredBalance).isLessThanZero()) { - throw new InsufficientAccountBalanceException("transactionAmount", getAccountBalance(), withdrawalFee, transactionAmount); - } - } - - if (this.getSavingsHoldAmount().compareTo(BigDecimal.ZERO) > 0) { - if (this.enforceMinRequiredBalance) { - if (runningBalance.minus(minRequiredBalance.plus(this.getSavingsHoldAmount())).isLessThanZero()) { - throw new InsufficientAccountBalanceException("transactionAmount", getAccountBalance(), withdrawalFee, - transactionAmount); - } - } else { - if (runningBalance.minus(this.getSavingsHoldAmount()).isLessThanZero()) { - throw new InsufficientAccountBalanceException("transactionAmount", getAccountBalance(), withdrawalFee, - transactionAmount); - } - } - } - } - - public void validateAccountBalanceDoesNotBecomeNegative(final String transactionAction, - final List depositAccountOnHoldTransactions, final boolean backdatedTxnsAllowedTill) { - - List transactionsSortedByDate = null; - BigDecimal transactionAmount = null; - if (backdatedTxnsAllowedTill) { - transactionsSortedByDate = retrieveSortedTransactions(); - } else { - transactionsSortedByDate = retrieveListOfTransactions(); - } - Money runningBalance = Money.zero(this.currency); - - if (backdatedTxnsAllowedTill) { - runningBalance = Money.of(this.currency, this.summary.getRunningBalanceOnPivotDate()); - } - - Money minRequiredBalance = minRequiredBalanceDerived(getCurrency()); - LocalDate lastSavingsDate = null; - for (final SavingsAccountTransaction transaction : transactionsSortedByDate) { - - transactionAmount = transaction.getAmount(); - - if (transaction.isNotReversed() && transaction.isCredit()) { - runningBalance = runningBalance.plus(transaction.getAmount(this.currency)); - } else if (transaction.isNotReversed() && transaction.isDebit()) { - runningBalance = runningBalance.minus(transaction.getAmount(this.currency)); - } - - /* - * Loop through the onHold funds and see if we need to deduct or add to minimum required balance and the - * point in time the transaction was made: - */ - if (depositAccountOnHoldTransactions != null) { - for (final DepositAccountOnHoldTransaction onHoldTransaction : depositAccountOnHoldTransactions) { - // Compare the balance of the on hold: - if (!DateUtils.isAfter(onHoldTransaction.getTransactionDate(), transaction.getTransactionDate()) - && (lastSavingsDate == null || DateUtils.isAfter(onHoldTransaction.getTransactionDate(), lastSavingsDate))) { - if (onHoldTransaction.getTransactionType().isHold()) { - minRequiredBalance = minRequiredBalance.plus(onHoldTransaction.getAmount(this.currency)); - } else { - minRequiredBalance = minRequiredBalance.minus(onHoldTransaction.getAmount(this.currency)); - } - } - } - } - - // enforceMinRequiredBalance - if (transaction.canProcessBalanceCheck()) { - if (runningBalance.minus(minRequiredBalance).isLessThanZero()) { - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(depositAccountType().resourceName() + transactionAction); - if (!this.allowOverdraft) { - baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("results.in.balance.going.negative"); - } - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - } - lastSavingsDate = transaction.getTransactionDate(); - } - - BigDecimal withdrawalFee = null; - if (isOverdraft()) { - if (runningBalance.minus(minRequiredBalance).isLessThanZero()) { - throw new InsufficientAccountBalanceException("transactionAmount", getAccountBalance(), withdrawalFee, transactionAmount); - } - } - } - - public void validateAccountBalanceDoesNotViolateOverdraft(final List savingsAccountTransaction, - final BigDecimal amountPaid) { - if (savingsAccountTransaction != null && savingsAccountTransaction.size() > 0) { - SavingsAccountTransaction savingsAccountTransactionFirst = savingsAccountTransaction.get(0); - if (!this.allowOverdraft) { - if (savingsAccountTransactionFirst.getRunningBalance(this.currency).minus(amountPaid).isLessThanZero()) { - throw new InsufficientAccountBalanceException("transactionAmount", getAccountBalance(), null, amountPaid); - } - } - - } else { - if (!this.allowOverdraft) { - throw new InsufficientAccountBalanceException("transactionAmount", BigDecimal.ZERO, null, amountPaid); - } - } - } - - protected boolean isAccountLocked(final LocalDate transactionDate) { - return DateUtils.isBefore(transactionDate, getLockedInUntilDate()); - } - - public LocalDate getLockedInUntilDate() { - return this.lockedInUntilDate; - } - - public BigDecimal getAccountBalance() { - return this.summary.getAccountBalance(this.currency).getAmount(); - } - - public void modifyApplication(final JsonCommand command, final Map actualChanges) { - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(SAVINGS_ACCOUNT_RESOURCE_NAME + SavingsApiConstants.modifyApplicationAction); - this.modifyApplication(command, actualChanges, baseDataValidator); - - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - public void modifyApplication(final JsonCommand command, final Map actualChanges, - final DataValidatorBuilder baseDataValidator) { - - final SavingsAccountStatusType currentStatus = SavingsAccountStatusType.fromInt(this.status); - if (!SavingsAccountStatusType.SUBMITTED_AND_PENDING_APPROVAL.hasStateOf(currentStatus)) { - baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("not.in.submittedandpendingapproval.state"); - return; - } - - final String localeAsInput = command.locale(); - final String dateFormat = command.dateFormat(); - - if (command.isChangeInLocalDateParameterNamed(SavingsApiConstants.submittedOnDateParamName, getSubmittedOnDate())) { - final String newValueAsString = command.stringValueOfParameterNamed(SavingsApiConstants.submittedOnDateParamName); - actualChanges.put(SavingsApiConstants.submittedOnDateParamName, newValueAsString); - actualChanges.put(SavingsApiConstants.localeParamName, localeAsInput); - actualChanges.put(SavingsApiConstants.dateFormatParamName, dateFormat); - this.submittedOnDate = command.localDateValueOfParameterNamed(SavingsApiConstants.submittedOnDateParamName); - } - if (command.isChangeInStringParameterNamed(SavingsApiConstants.accountNoParamName, this.accountNumber)) { - final String newValue = command.stringValueOfParameterNamed(SavingsApiConstants.accountNoParamName); - actualChanges.put(SavingsApiConstants.accountNoParamName, newValue); - this.accountNumber = StringUtils.defaultIfEmpty(newValue, null); - } - if (command.isChangeInStringParameterNamed(SavingsApiConstants.externalIdParamName, this.externalId.getValue())) { - final String newValue = command.stringValueOfParameterNamed(SavingsApiConstants.externalIdParamName); - actualChanges.put(SavingsApiConstants.externalIdParamName, newValue); - this.externalId = ExternalIdFactory.produce(newValue); - if (this.externalId.isEmpty() && TemporaryConfigurationServiceContainer.isExternalIdAutoGenerationEnabled()) { - this.externalId = ExternalId.generate(); - } - } - if (command.isChangeInLongParameterNamed(SavingsApiConstants.clientIdParamName, clientId())) { - final Long newValue = command.longValueOfParameterNamed(SavingsApiConstants.clientIdParamName); - actualChanges.put(SavingsApiConstants.clientIdParamName, newValue); - } - if (command.isChangeInLongParameterNamed(SavingsApiConstants.groupIdParamName, groupId())) { - final Long newValue = command.longValueOfParameterNamed(SavingsApiConstants.groupIdParamName); - actualChanges.put(SavingsApiConstants.groupIdParamName, newValue); - } - if (command.isChangeInLongParameterNamed(SavingsApiConstants.productIdParamName, this.product.getId())) { - final Long newValue = command.longValueOfParameterNamed(SavingsApiConstants.productIdParamName); - actualChanges.put(SavingsApiConstants.productIdParamName, newValue); - } - if (command.isChangeInLongParameterNamed(SavingsApiConstants.fieldOfficerIdParamName, hasSavingsOfficerId())) { - final Long newValue = command.longValueOfParameterNamed(SavingsApiConstants.fieldOfficerIdParamName); - actualChanges.put(SavingsApiConstants.fieldOfficerIdParamName, newValue); - } - if (command.isChangeInBigDecimalParameterNamed(SavingsApiConstants.nominalAnnualInterestRateParamName, - this.nominalAnnualInterestRate)) { - final BigDecimal newValue = command.bigDecimalValueOfParameterNamed(SavingsApiConstants.nominalAnnualInterestRateParamName); - actualChanges.put(SavingsApiConstants.nominalAnnualInterestRateParamName, newValue); - this.nominalAnnualInterestRate = newValue; - } - if (command.isChangeInIntegerParameterNamed(SavingsApiConstants.interestCompoundingPeriodTypeParamName, - this.interestCompoundingPeriodType)) { - final Integer newValue = command.integerValueOfParameterNamed(SavingsApiConstants.interestCompoundingPeriodTypeParamName); - this.interestCompoundingPeriodType = newValue != null ? SavingsCompoundingInterestPeriodType.fromInt(newValue).getValue() - : newValue; - actualChanges.put(SavingsApiConstants.interestCompoundingPeriodTypeParamName, this.interestCompoundingPeriodType); - } - if (command.isChangeInIntegerParameterNamed(SavingsApiConstants.interestPostingPeriodTypeParamName, - this.interestPostingPeriodType)) { - final Integer newValue = command.integerValueOfParameterNamed(SavingsApiConstants.interestPostingPeriodTypeParamName); - this.interestPostingPeriodType = newValue != null ? SavingsPostingInterestPeriodType.fromInt(newValue).getValue() : newValue; - actualChanges.put(SavingsApiConstants.interestPostingPeriodTypeParamName, this.interestPostingPeriodType); - } - if (command.isChangeInIntegerParameterNamed(SavingsApiConstants.interestCalculationTypeParamName, this.interestCalculationType)) { - final Integer newValue = command.integerValueOfParameterNamed(SavingsApiConstants.interestCalculationTypeParamName); - this.interestCalculationType = newValue != null ? SavingsInterestCalculationType.fromInt(newValue).getValue() : newValue; - actualChanges.put(SavingsApiConstants.interestCalculationTypeParamName, this.interestCalculationType); - } - if (command.isChangeInIntegerParameterNamed(SavingsApiConstants.interestCalculationDaysInYearTypeParamName, - this.interestCalculationDaysInYearType)) { - final Integer newValue = command.integerValueOfParameterNamed(SavingsApiConstants.interestCalculationDaysInYearTypeParamName); - this.interestCalculationDaysInYearType = newValue != null - ? SavingsInterestCalculationDaysInYearType.fromInt(newValue).getValue() - : newValue; - actualChanges.put(SavingsApiConstants.interestCalculationDaysInYearTypeParamName, this.interestCalculationDaysInYearType); - } - if (command.isChangeInBigDecimalParameterNamedDefaultingZeroToNull(SavingsApiConstants.minRequiredOpeningBalanceParamName, - this.minRequiredOpeningBalance)) { - final BigDecimal newValue = command - .bigDecimalValueOfParameterNamedDefaultToNullIfZero(SavingsApiConstants.minRequiredOpeningBalanceParamName); - actualChanges.put(SavingsApiConstants.minRequiredOpeningBalanceParamName, newValue); - actualChanges.put("locale", localeAsInput); - this.minRequiredOpeningBalance = Money.of(this.currency, newValue).getAmount(); - } - if (command.isChangeInIntegerParameterNamedDefaultingZeroToNull(SavingsApiConstants.lockinPeriodFrequencyParamName, - this.lockinPeriodFrequency)) { - final Integer newValue = command - .integerValueOfParameterNamedDefaultToNullIfZero(SavingsApiConstants.lockinPeriodFrequencyParamName); - actualChanges.put(SavingsApiConstants.lockinPeriodFrequencyParamName, newValue); - actualChanges.put("locale", localeAsInput); - this.lockinPeriodFrequency = newValue; - } - - if (command.isChangeInIntegerParameterNamed(SavingsApiConstants.lockinPeriodFrequencyTypeParamName, - this.lockinPeriodFrequencyType)) { - final Integer newValue = command.integerValueOfParameterNamed(SavingsApiConstants.lockinPeriodFrequencyTypeParamName); - actualChanges.put(SavingsApiConstants.lockinPeriodFrequencyTypeParamName, newValue); - this.lockinPeriodFrequencyType = newValue != null ? SavingsPeriodFrequencyType.fromInt(newValue).getValue() : newValue; - } - - // set period type to null if frequency is null - if (this.lockinPeriodFrequency == null) { - this.lockinPeriodFrequencyType = null; - } - - if (command.isChangeInBooleanParameterNamed(withdrawalFeeForTransfersParamName, this.withdrawalFeeApplicableForTransfer)) { - final boolean newValue = command.booleanPrimitiveValueOfParameterNamed(withdrawalFeeForTransfersParamName); - actualChanges.put(withdrawalFeeForTransfersParamName, newValue); - this.withdrawalFeeApplicableForTransfer = newValue; - } - - // charges - final String chargesParamName = "charges"; - if (command.hasParameter(chargesParamName)) { - final JsonArray jsonArray = command.arrayOfParameterNamed(chargesParamName); - if (jsonArray != null) { - actualChanges.put(chargesParamName, command.jsonFragment(chargesParamName)); - } - } - - if (command.isChangeInBooleanParameterNamed(allowOverdraftParamName, this.allowOverdraft)) { - final boolean newValue = command.booleanPrimitiveValueOfParameterNamed(allowOverdraftParamName); - actualChanges.put(allowOverdraftParamName, newValue); - this.allowOverdraft = newValue; - } - - if (command.isChangeInBigDecimalParameterNamedDefaultingZeroToNull(overdraftLimitParamName, this.overdraftLimit)) { - final BigDecimal newValue = command.bigDecimalValueOfParameterNamedDefaultToNullIfZero(overdraftLimitParamName); - actualChanges.put(overdraftLimitParamName, newValue); - actualChanges.put(localeParamName, localeAsInput); - this.overdraftLimit = newValue; - } - - if (command.isChangeInBigDecimalParameterNamedDefaultingZeroToNull(nominalAnnualInterestRateOverdraftParamName, - this.nominalAnnualInterestRateOverdraft)) { - final BigDecimal newValue = command - .bigDecimalValueOfParameterNamedDefaultToNullIfZero(nominalAnnualInterestRateOverdraftParamName); - actualChanges.put(nominalAnnualInterestRateOverdraftParamName, newValue); - actualChanges.put(localeParamName, localeAsInput); - this.nominalAnnualInterestRateOverdraft = newValue; - } - - if (command.isChangeInBigDecimalParameterNamedDefaultingZeroToNull(minOverdraftForInterestCalculationParamName, - this.minOverdraftForInterestCalculation)) { - final BigDecimal newValue = command - .bigDecimalValueOfParameterNamedDefaultToNullIfZero(minOverdraftForInterestCalculationParamName); - actualChanges.put(minOverdraftForInterestCalculationParamName, newValue); - actualChanges.put(localeParamName, localeAsInput); - this.minOverdraftForInterestCalculation = newValue; - } - - if (!this.allowOverdraft) { - this.overdraftLimit = null; - this.nominalAnnualInterestRateOverdraft = null; - this.minOverdraftForInterestCalculation = null; - } - - if (command.isChangeInBooleanParameterNamed(enforceMinRequiredBalanceParamName, this.enforceMinRequiredBalance)) { - final boolean newValue = command.booleanPrimitiveValueOfParameterNamed(enforceMinRequiredBalanceParamName); - actualChanges.put(enforceMinRequiredBalanceParamName, newValue); - this.enforceMinRequiredBalance = newValue; - } - if (command.isChangeInBigDecimalParameterNamedDefaultingZeroToNull(minRequiredBalanceParamName, this.minRequiredBalance)) { - final BigDecimal newValue = command.bigDecimalValueOfParameterNamedDefaultToNullIfZero(minRequiredBalanceParamName); - actualChanges.put(minRequiredBalanceParamName, newValue); - actualChanges.put(localeParamName, localeAsInput); - this.minRequiredBalance = newValue; - } - if (command.isChangeInBooleanParameterNamed(lienAllowedParamName, this.lienAllowed)) { - final boolean newValue = command.booleanPrimitiveValueOfParameterNamed(lienAllowedParamName); - actualChanges.put(lienAllowedParamName, newValue); - this.lienAllowed = newValue; - } - if (command.isChangeInBigDecimalParameterNamedDefaultingZeroToNull(maxAllowedLienLimitParamName, this.maxAllowedLienLimit)) { - final BigDecimal newValue = command.bigDecimalValueOfParameterNamedDefaultToNullIfZero(maxAllowedLienLimitParamName); - actualChanges.put(maxAllowedLienLimitParamName, newValue); - actualChanges.put(localeParamName, localeAsInput); - this.maxAllowedLienLimit = newValue; - } - if (command.isChangeInBooleanParameterNamed(withHoldTaxParamName, this.withHoldTax)) { - final boolean newValue = command.booleanPrimitiveValueOfParameterNamed(withHoldTaxParamName); - actualChanges.put(withHoldTaxParamName, newValue); - this.withHoldTax = newValue; - if (this.withHoldTax && this.taxGroup == null) { - baseDataValidator.reset().parameter(withHoldTaxParamName).failWithCode("not.supported.for.this.account"); - } - } - validateLockinDetails(baseDataValidator); - esnureOverdraftLimitsSetForOverdraftAccounts(); - } - - /** - * If overdrafts are allowed and the overdraft limit is not set, set the same to Zero - **/ - private void esnureOverdraftLimitsSetForOverdraftAccounts() { - - this.overdraftLimit = this.overdraftLimit == null ? BigDecimal.ZERO : this.overdraftLimit; - this.nominalAnnualInterestRateOverdraft = this.nominalAnnualInterestRateOverdraft == null ? BigDecimal.ZERO - : this.nominalAnnualInterestRateOverdraft; - this.minOverdraftForInterestCalculation = this.minOverdraftForInterestCalculation == null ? BigDecimal.ZERO - : this.minOverdraftForInterestCalculation; - } - - private void validateLockinDetails(final DataValidatorBuilder baseDataValidator) { - - /* - * final List dataValidationErrors = new ArrayList(); final - * DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - * .resource(resourceName); - */ - - if (this.lockinPeriodFrequency == null) { - baseDataValidator.reset().parameter(lockinPeriodFrequencyTypeParamName).value(this.lockinPeriodFrequencyType).ignoreIfNull() - .inMinMaxRange(0, 3); - - if (this.lockinPeriodFrequencyType != null) { - baseDataValidator.reset().parameter(lockinPeriodFrequencyParamName).value(this.lockinPeriodFrequency).ignoreIfNull() - .integerZeroOrGreater(); - } - } else { - baseDataValidator.reset().parameter(lockinPeriodFrequencyParamName).value(this.lockinPeriodFrequencyType) - .integerZeroOrGreater(); - baseDataValidator.reset().parameter(lockinPeriodFrequencyTypeParamName).value(this.lockinPeriodFrequencyType).ignoreIfNull() - .inMinMaxRange(0, 3); - } - } - - public Map deriveAccountingBridgeData(final String currencyCode, final Set existingTransactionIds, - final Set existingReversedTransactionIds, boolean isAccountTransfer, final boolean backdatedTxnsAllowedTill) { - - final Map accountingBridgeData = new LinkedHashMap<>(); - accountingBridgeData.put("savingsId", getId()); - accountingBridgeData.put("savingsProductId", productId()); - accountingBridgeData.put("currencyCode", currencyCode); - accountingBridgeData.put("officeId", officeId()); - accountingBridgeData.put("cashBasedAccountingEnabled", isCashBasedAccountingEnabledOnSavingsProduct()); - accountingBridgeData.put("accrualBasedAccountingEnabled", isAccrualBasedAccountingEnabledOnSavingsProduct()); - accountingBridgeData.put("isAccountTransfer", isAccountTransfer); - - final List> newSavingsTransactions = new ArrayList<>(); - - List trans = null; - - if (backdatedTxnsAllowedTill) { - trans = getSavingsAccountTransactionsWithPivotConfig(); - } else { - trans = getTransactions(); - } - - // Adding new transactions to the array - for (final SavingsAccountTransaction transaction : trans) { - if (transaction.isReversed() && !existingReversedTransactionIds.contains(transaction.getId())) { - newSavingsTransactions.add(transaction.toMapData(currencyCode)); - } else if (!existingTransactionIds.contains(transaction.getId())) { - newSavingsTransactions.add(transaction.toMapData(currencyCode)); - } - } - - accountingBridgeData.put("newSavingsTransactions", newSavingsTransactions); - return accountingBridgeData; - } - - public Collection findExistingTransactionIds() { - final Collection ids = new ArrayList<>(); - List trans = getTransactions(); - for (final SavingsAccountTransaction transaction : trans) { - ids.add(transaction.getId()); - } - return ids; - } - - public Collection findCurrentTransactionIdsWithPivotDateConfig() { - - final Collection ids = new ArrayList<>(); - List trans = getSavingsAccountTransactionsWithPivotConfig(); - for (final SavingsAccountTransaction transaction : trans) { - ids.add(transaction.getId()); - } - return ids; - } - - public Collection findExistingReversedTransactionIds() { - - final Collection ids = new ArrayList<>(); - List trans = getTransactions(); - // time consuming - for (final SavingsAccountTransaction transaction : trans) { - if (transaction.isReversed()) { - ids.add(transaction.getId()); - } - } - - return ids; - } - - public Collection findCurrentReversedTransactionIdsWithPivotDateConfig() { - final Collection ids = new ArrayList<>(); - List trans = getSavingsAccountTransactionsWithPivotConfig(); - // time consuming - for (final SavingsAccountTransaction transaction : trans) { - if (transaction.isReversed()) { - ids.add(transaction.getId()); - } - } - return ids; - } - - public void update(final Client client) { - this.client = client; - } - - public void update(final Group group) { - this.group = group; - } - - public void update(final SavingsProduct product) { - this.product = product; - this.minBalanceForInterestCalculation = product.minBalanceForInterestCalculation(); - } - - public void update(final Staff savingsOfficer) { - this.savingsOfficer = savingsOfficer; - } - - public void updateAccountNo(final String newAccountNo) { - this.accountNumber = newAccountNo; - this.accountNumberRequiresAutoGeneration = false; - } - - public boolean isAccountNumberRequiresAutoGeneration() { - return this.accountNumberRequiresAutoGeneration; - } - - public Long productId() { - return this.product.getId(); - } - - public SavingsProduct savingsProduct() { - return this.product; - } - - private Boolean isCashBasedAccountingEnabledOnSavingsProduct() { - return this.product.isCashBasedAccountingEnabled(); - } - - private Boolean isAccrualBasedAccountingEnabledOnSavingsProduct() { - return this.product.isAccrualBasedAccountingEnabled(); - } - - public Long officeId() { - Long officeId = null; - if (this.client != null) { - officeId = this.client.officeId(); - } else if (this.group != null) { - officeId = this.group.officeId(); - } - return officeId; - } - - public Office office() { - Office office = null; - if (this.client != null) { - office = this.client.getOffice(); - } else if (this.group != null) { - office = this.group.getOffice(); - } - return office; - } - - public Staff getSavingsOfficer() { - return this.savingsOfficer; - } - - public Boolean getEnforceMinRequiredBalance() { - return this.enforceMinRequiredBalance; - } - - public Boolean getLienAllowed() { - return this.lienAllowed; - } - - public void unassignSavingsOfficer() { - this.savingsOfficer = null; - } - - public void assignSavingsOfficer(final Staff fieldOfficer) { - this.savingsOfficer = fieldOfficer; - } - - public Long clientId() { - Long id = null; - if (this.client != null) { - id = this.client.getId(); - } - return id; - } - - public Long groupId() { - Long id = null; - if (this.group != null) { - id = this.group.getId(); - } - return id; - } - - public GroupSavingsIndividualMonitoring getGsim() { - return gsim; - } - - public Long getSavingsProductId() { - return this.savingsProduct().getId(); - } - - public void setGsim(GroupSavingsIndividualMonitoring gsim) { - this.gsim = gsim; - } - - public Long hasSavingsOfficerId() { - Long id = null; - if (this.savingsOfficer != null) { - id = this.savingsOfficer.getId(); - } - return id; - } - - public boolean hasSavingsOfficer(final Staff fromSavingsOfficer) { - - boolean matchesCurrentSavingsOfficer = false; - if (this.savingsOfficer != null) { - matchesCurrentSavingsOfficer = this.savingsOfficer.identifiedBy(fromSavingsOfficer); - } else { - matchesCurrentSavingsOfficer = fromSavingsOfficer == null; - } - return matchesCurrentSavingsOfficer; - } - - public void reassignSavingsOfficer(final Staff newSavingsOfficer, final LocalDate assignmentDate) { - final SavingsOfficerAssignmentHistory latestHistoryRecord = findLatestIncompleteHistoryRecord(); - final SavingsOfficerAssignmentHistory lastAssignmentRecord = findLastAssignmentHistoryRecord(newSavingsOfficer); - - // assignment date should not be less than savings account submitted date - if (DateUtils.isBefore(assignmentDate, getSubmittedOnDate())) { - final String errorMessage = "The Savings Officer assignment date (" + assignmentDate.toString() - + ") cannot be before savings submitted date (" + getSubmittedOnDate().toString() + ")."; - throw new SavingsOfficerAssignmentDateException("cannot.be.before.savings.submitted.date", errorMessage, assignmentDate, - getSubmittedOnDate()); - } else if (lastAssignmentRecord != null && lastAssignmentRecord.isBeforeEndDate(assignmentDate)) { - final String errorMessage = "The Savings Officer assignment date (" + assignmentDate - + ") cannot be before previous Savings Officer unassigned date (" + lastAssignmentRecord.getEndDate() + ")."; - throw new SavingsOfficerAssignmentDateException("cannot.be.before.previous.unassignement.date", errorMessage, assignmentDate, - lastAssignmentRecord.getEndDate()); - } else if (DateUtils.isDateInTheFuture(assignmentDate)) { - final String errorMessage = "The Savings Officer assignment date (" + assignmentDate + ") cannot be in the future."; - throw new SavingsOfficerAssignmentDateException("cannot.be.a.future.date", errorMessage, assignmentDate); - } else if (latestHistoryRecord != null && this.savingsOfficer.identifiedBy(newSavingsOfficer)) { - latestHistoryRecord.setStartDate(assignmentDate); - } else if (latestHistoryRecord != null && latestHistoryRecord.matchesStartDateOf(assignmentDate)) { - latestHistoryRecord.setSavingsOfficer(newSavingsOfficer); - this.savingsOfficer = newSavingsOfficer; - } else if (latestHistoryRecord != null && latestHistoryRecord.isBeforeStartDate(assignmentDate)) { - final String errorMessage = "Savings account with identifier " + getId() + " was already assigned before date " - + assignmentDate; - throw new SavingsOfficerAssignmentDateException("is.before.last.assignment.date", errorMessage, getId(), assignmentDate); - } else { - if (latestHistoryRecord != null) { - // savings officer correctly changed from previous savings - // officer to - // new savings officer - latestHistoryRecord.setEndDate(assignmentDate); - } - this.savingsOfficer = newSavingsOfficer; - if (isNotSubmittedAndPendingApproval()) { - final SavingsOfficerAssignmentHistory savingsOfficerAssignmentHistory = SavingsOfficerAssignmentHistory.createNew(this, - this.savingsOfficer, assignmentDate); - this.savingsOfficerHistory.add(savingsOfficerAssignmentHistory); - } - } - } - - private SavingsOfficerAssignmentHistory findLastAssignmentHistoryRecord(final Staff newSavingsOfficer) { - SavingsOfficerAssignmentHistory lastAssignmentRecordLatestEndDate = null; - for (final SavingsOfficerAssignmentHistory historyRecord : this.savingsOfficerHistory) { - if (historyRecord.isCurrentRecord() && !historyRecord.isSameSavingsOfficer(newSavingsOfficer)) { - lastAssignmentRecordLatestEndDate = historyRecord; - break; - } - - if (lastAssignmentRecordLatestEndDate == null) { - lastAssignmentRecordLatestEndDate = historyRecord; - } else if (historyRecord.isBeforeEndDate(lastAssignmentRecordLatestEndDate.getEndDate()) - && !historyRecord.isSameSavingsOfficer(newSavingsOfficer)) { - lastAssignmentRecordLatestEndDate = historyRecord; - } - } - return lastAssignmentRecordLatestEndDate; - } - - public LocalDate getSubmittedOnDate() { - return submittedOnDate; - } - - public LocalDate getApprovedOnDate() { - return approvedOnDate; - } - - public LocalDate getRejectedOnDate() { - return rejectedOnDate; - } - - public AppUser getRejectedBy() { - return this.rejectedBy; - } - - public void removeSavingsOfficer(final LocalDate unassignDate) { - final SavingsOfficerAssignmentHistory latestHistoryRecord = findLatestIncompleteHistoryRecord(); - - if (latestHistoryRecord != null) { - validateUnassignDate(latestHistoryRecord, unassignDate); - latestHistoryRecord.setEndDate(unassignDate); - } - this.savingsOfficer = null; - } - - private SavingsOfficerAssignmentHistory findLatestIncompleteHistoryRecord() { - SavingsOfficerAssignmentHistory latestRecordWithNoEndDate = null; - for (final SavingsOfficerAssignmentHistory historyRecord : this.savingsOfficerHistory) { - if (historyRecord.isCurrentRecord()) { - latestRecordWithNoEndDate = historyRecord; - break; - } - } - return latestRecordWithNoEndDate; - } - - private void validateUnassignDate(final SavingsOfficerAssignmentHistory latestHistoryRecord, final LocalDate unassignDate) { - if (DateUtils.isBefore(unassignDate, latestHistoryRecord.getStartDate())) { - final String errorMessage = "The Savings officer Unassign date(" + unassignDate + ") cannot be before its assignment date (" - + latestHistoryRecord.getStartDate() + ")."; - throw new SavingsOfficerUnassignmentDateException("cannot.be.before.assignment.date", errorMessage, getId(), - getSavingsOfficer().getId(), latestHistoryRecord.getStartDate(), unassignDate); - } else if (DateUtils.isDateInTheFuture(unassignDate)) { - final String errorMessage = "The Savings Officer Unassign date (" + unassignDate + ") cannot be in the future."; - throw new SavingsOfficerUnassignmentDateException("cannot.be.a.future.date", errorMessage, unassignDate); - } - } - - public MonetaryCurrency getCurrency() { - return this.currency; - } - - public void validateNewApplicationState(final String resourceName) { - // validateWithdrawalFeeDetails(); - // validateAnnualFeeDetails(); - - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(resourceName + SavingsApiConstants.summitalAction); - - validateLockinDetails(baseDataValidator); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - - final LocalDate submittedOn = getSubmittedOnDate(); - if (DateUtils.isDateInTheFuture(submittedOn)) { - baseDataValidator.reset().parameter(SavingsApiConstants.submittedOnDateParamName).value(submittedOn) - .failWithCodeNoParameterAddedToErrorCode("cannot.be.a.future.date"); - } - - if (this.client != null && this.client.isActivatedAfter(submittedOn)) { - baseDataValidator.reset().parameter(SavingsApiConstants.submittedOnDateParamName).value(this.client.getActivationDate()) - .failWithCodeNoParameterAddedToErrorCode("cannot.be.before.client.activation.date"); - } else if (this.group != null && this.group.isActivatedAfter(submittedOn)) { - baseDataValidator.reset().parameter(SavingsApiConstants.submittedOnDateParamName).value(this.group.getActivationDate()) - .failWithCodeNoParameterAddedToErrorCode("cannot.be.before.client.activation.date"); - } - - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - public Client getClient() { - return this.client; - } - - public BigDecimal getNominalAnnualInterestRate() { - return this.nominalAnnualInterestRate; - } - - public Integer getInterestCompoundingPeriodType() { - return this.interestCompoundingPeriodType; - } - - public Integer getInterestPostingPeriodType() { - return this.interestPostingPeriodType; - } - - public Integer getInterestCalculationType() { - return this.interestCalculationType; - } - - public BigDecimal getNominalAnnualInterestRateOverdraft() { - return this.nominalAnnualInterestRateOverdraft; - } - - public Map approveApplication(final AppUser currentUser, final JsonCommand command) { - final Map actualChanges = new LinkedHashMap<>(); - - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(SAVINGS_ACCOUNT_RESOURCE_NAME + SavingsApiConstants.approvalAction); - - final SavingsAccountStatusType currentStatus = getStatus(); - if (!SavingsAccountStatusType.SUBMITTED_AND_PENDING_APPROVAL.hasStateOf(currentStatus)) { - baseDataValidator.reset().parameter(SavingsApiConstants.approvedOnDateParamName) - .failWithCodeNoParameterAddedToErrorCode("not.in.submittedandpendingapproval.state"); - - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - this.status = SavingsAccountStatusType.APPROVED.getValue(); - actualChanges.put(SavingsApiConstants.statusParamName, SavingsEnumerations.status(this.status)); - - // only do below if status has changed in the 'approval' case - final LocalDate approvedOn = command.localDateValueOfParameterNamed(SavingsApiConstants.approvedOnDateParamName); - final String approvedOnDateChange = command.stringValueOfParameterNamed(SavingsApiConstants.approvedOnDateParamName); - - this.approvedOnDate = approvedOn; - this.approvedBy = currentUser; - actualChanges.put(SavingsApiConstants.localeParamName, command.locale()); - actualChanges.put(SavingsApiConstants.dateFormatParamName, command.dateFormat()); - actualChanges.put(SavingsApiConstants.approvedOnDateParamName, approvedOnDateChange); - - final LocalDate submittalDate = getSubmittedOnDate(); - if (DateUtils.isBefore(approvedOn, submittalDate)) { - final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(command.extractLocale()); - final String submittalDateAsString = formatter.format(submittalDate); - - baseDataValidator.reset().parameter(SavingsApiConstants.approvedOnDateParamName).value(submittalDateAsString) - .failWithCodeNoParameterAddedToErrorCode("cannot.be.before.submittal.date"); - - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - if (DateUtils.isAfterBusinessDate(approvedOn)) { - baseDataValidator.reset().parameter(SavingsApiConstants.approvedOnDateParamName) - .failWithCodeNoParameterAddedToErrorCode("cannot.be.a.future.date"); - - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - validateActivityNotBeforeClientOrGroupTransferDate(SavingsEvent.SAVINGS_APPLICATION_APPROVED, approvedOn); - - // FIXME - kw - support field officer history for savings accounts - // if (this.fieldOfficer != null) { - // final LoanOfficerAssignmentHistory loanOfficerAssignmentHistory = - // LoanOfficerAssignmentHistory.createNew(this, - // this.fieldOfficer, approvedOn); - // this.loanOfficerHistory.add(loanOfficerAssignmentHistory); - // } - if (this.savingsOfficer != null) { - final SavingsOfficerAssignmentHistory savingsOfficerAssignmentHistory = SavingsOfficerAssignmentHistory.createNew(this, - this.savingsOfficer, approvedOn); - this.savingsOfficerHistory.add(savingsOfficerAssignmentHistory); - } - return actualChanges; - } - - public Map undoApplicationApproval() { - final Map actualChanges = new LinkedHashMap<>(); - - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(SAVINGS_ACCOUNT_RESOURCE_NAME + SavingsApiConstants.undoApprovalAction); - - final SavingsAccountStatusType currentStatus = getStatus(); - if (!SavingsAccountStatusType.APPROVED.hasStateOf(currentStatus)) { - - baseDataValidator.reset().parameter(SavingsApiConstants.approvedOnDateParamName) - .failWithCodeNoParameterAddedToErrorCode("not.in.approved.state"); - - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - this.status = SavingsAccountStatusType.SUBMITTED_AND_PENDING_APPROVAL.getValue(); - actualChanges.put(SavingsApiConstants.statusParamName, SavingsEnumerations.status(this.status)); - - this.approvedOnDate = null; - this.approvedBy = null; - this.rejectedOnDate = null; - this.rejectedBy = null; - this.withdrawnOnDate = null; - this.withdrawnBy = null; - this.closedOnDate = null; - this.closedBy = null; - actualChanges.put(SavingsApiConstants.approvedOnDateParamName, ""); - - // FIXME - kw - support field officer history for savings accounts - // this.loanOfficerHistory.clear(); - - return actualChanges; - } - - public void undoTransaction(final Long transactionId) { - - SavingsAccountTransaction transactionToUndo = null; - for (final SavingsAccountTransaction transaction : this.transactions) { - if (transaction.isIdentifiedBy(transactionId)) { - transactionToUndo = transaction; - } - } - - if (transactionToUndo == null) { - throw new SavingsAccountTransactionNotFoundException(this.getId(), transactionId); - } - - validateAttemptToUndoTransferRelatedTransactions(transactionToUndo); - validateActivityNotBeforeClientOrGroupTransferDate(SavingsEvent.SAVINGS_UNDO_TRANSACTION, transactionToUndo.getTransactionDate()); - transactionToUndo.reverse(); - if (transactionToUndo.isChargeTransaction() || transactionToUndo.isWaiveCharge()) { - // undo charge - final Set chargesPaidBy = transactionToUndo.getSavingsAccountChargesPaid(); - for (final SavingsAccountChargePaidBy savingsAccountChargePaidBy : chargesPaidBy) { - final SavingsAccountCharge chargeToUndo = savingsAccountChargePaidBy.getSavingsAccountCharge(); - if (transactionToUndo.isChargeTransaction()) { - chargeToUndo.undoPayment(this.getCurrency(), transactionToUndo.getAmount(this.getCurrency())); - } else if (transactionToUndo.isWaiveCharge()) { - chargeToUndo.undoWaiver(this.getCurrency(), transactionToUndo.getAmount(this.getCurrency())); - } - } - } - } - - public void undoSavingsTransaction(final Long transactionId) { - - SavingsAccountTransaction transactionToUndo = null; - for (final SavingsAccountTransaction transaction : this.savingsAccountTransactions) { - if (transaction.isIdentifiedBy(transactionId)) { - transactionToUndo = transaction; - } - } - - if (transactionToUndo == null) { - throw new SavingsAccountTransactionNotFoundException(this.getId(), transactionId); - } - - validateAttemptToUndoTransferRelatedTransactions(transactionToUndo); - validateActivityNotBeforeClientOrGroupTransferDate(SavingsEvent.SAVINGS_UNDO_TRANSACTION, transactionToUndo.getTransactionDate()); - transactionToUndo.reverse(); - if (transactionToUndo.isChargeTransaction() || transactionToUndo.isWaiveCharge()) { - // undo charge - final Set chargesPaidBy = transactionToUndo.getSavingsAccountChargesPaid(); - for (final SavingsAccountChargePaidBy savingsAccountChargePaidBy : chargesPaidBy) { - final SavingsAccountCharge chargeToUndo = savingsAccountChargePaidBy.getSavingsAccountCharge(); - if (transactionToUndo.isChargeTransaction()) { - chargeToUndo.undoPayment(this.getCurrency(), transactionToUndo.getAmount(this.currency)); - } else if (transactionToUndo.isWaiveCharge()) { - chargeToUndo.undoWaiver(this.getCurrency(), transactionToUndo.getAmount(this.currency)); - } - } - } - } - - public void undoTransaction(final SavingsAccountTransaction transactionToUndo) { - - if (transactionToUndo.isReversed()) { - throw new SavingsAccountTransactionNotFoundException(this.getId(), transactionToUndo.getId()); - } - - validateAttemptToUndoTransferRelatedTransactions(transactionToUndo); - validateActivityNotBeforeClientOrGroupTransferDate(SavingsEvent.SAVINGS_UNDO_TRANSACTION, transactionToUndo.getTransactionDate()); - transactionToUndo.reverse(); - if (transactionToUndo.isChargeTransaction() || transactionToUndo.isWaiveCharge()) { - // undo charge - final Set chargesPaidBy = transactionToUndo.getSavingsAccountChargesPaid(); - for (final SavingsAccountChargePaidBy savingsAccountChargePaidBy : chargesPaidBy) { - final SavingsAccountCharge chargeToUndo = savingsAccountChargePaidBy.getSavingsAccountCharge(); - if (transactionToUndo.isChargeTransaction()) { - chargeToUndo.undoPayment(this.getCurrency(), transactionToUndo.getAmount(this.currency)); - } else if (transactionToUndo.isWaiveCharge()) { - chargeToUndo.undoWaiver(this.getCurrency(), transactionToUndo.getAmount(this.currency)); - } - } - } - } - - private LocalDate findLatestAnnualFeeTransactionDueDate() { - LocalDate nextDueDate = null; - - LocalDate lastAnnualFeeTransactionDate = null; - for (final SavingsAccountTransaction transaction : retreiveOrderedNonInterestPostingTransactions()) { - if (transaction.isAnnualFeeAndNotReversed()) { - LocalDate transactionDate = transaction.getTransactionDate(); - if (lastAnnualFeeTransactionDate == null) { - lastAnnualFeeTransactionDate = transactionDate; - nextDueDate = lastAnnualFeeTransactionDate; - } - if (DateUtils.isAfter(transactionDate, lastAnnualFeeTransactionDate)) { - lastAnnualFeeTransactionDate = transactionDate; - nextDueDate = lastAnnualFeeTransactionDate; - } - } - } - return nextDueDate; - } - - public void validateAccountBalanceDoesNotBecomeNegativeMinimal(final BigDecimal transactionAmount, final boolean isException) { - // final List transactionsSortedByDate = retrieveListOfTransactions(); - Money runningBalance = this.summary.getAccountBalance(getCurrency()); - Money minRequiredBalance = minRequiredBalanceDerived(getCurrency()); - final BigDecimal withdrawalFee = null; - - // check last txn date - - // In overdraft cases, minRequiredBalance can be in violation after interest posting - // and should be checked after processing all transactions - if (!isOverdraft()) { - if (runningBalance.minus(minRequiredBalance).isLessThanZero()) { - throw new InsufficientAccountBalanceException("transactionAmount", getAccountBalance(), withdrawalFee, transactionAmount); - } - } - - if (this.getSavingsHoldAmount().compareTo(BigDecimal.ZERO) > 0) { - if (runningBalance.minus(this.getSavingsHoldAmount()).minus(minRequiredBalance).isLessThanZero()) { - throw new InsufficientAccountBalanceException("transactionAmount", getAccountBalance(), withdrawalFee, transactionAmount); - } - } - } - - public Map rejectApplication(final AppUser currentUser, final JsonCommand command) { - final Map actualChanges = new LinkedHashMap<>(); - - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(SAVINGS_ACCOUNT_RESOURCE_NAME + SavingsApiConstants.rejectAction); - - final SavingsAccountStatusType currentStatus = getStatus(); - if (!SavingsAccountStatusType.SUBMITTED_AND_PENDING_APPROVAL.hasStateOf(currentStatus)) { - - baseDataValidator.reset().parameter(SavingsApiConstants.rejectedOnDateParamName) - .failWithCodeNoParameterAddedToErrorCode("not.in.submittedandpendingapproval.state"); - - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - this.status = SavingsAccountStatusType.REJECTED.getValue(); - actualChanges.put(SavingsApiConstants.statusParamName, SavingsEnumerations.status(this.status)); - - final LocalDate rejectedOn = command.localDateValueOfParameterNamed(SavingsApiConstants.rejectedOnDateParamName); - final String rejectedOnAsString = command.stringValueOfParameterNamed(SavingsApiConstants.rejectedOnDateParamName); - - this.rejectedOnDate = rejectedOn; - this.rejectedBy = currentUser; - this.withdrawnOnDate = null; - this.withdrawnBy = null; - this.closedOnDate = rejectedOn; - this.closedBy = currentUser; - - actualChanges.put(SavingsApiConstants.localeParamName, command.locale()); - actualChanges.put(SavingsApiConstants.dateFormatParamName, command.dateFormat()); - actualChanges.put(SavingsApiConstants.rejectedOnDateParamName, rejectedOnAsString); - actualChanges.put(SavingsApiConstants.closedOnDateParamName, rejectedOnAsString); - - final LocalDate submittalDate = getSubmittedOnDate(); - if (DateUtils.isBefore(rejectedOn, submittalDate)) { - final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(command.extractLocale()); - final String submittalDateAsString = formatter.format(submittalDate); - - baseDataValidator.reset().parameter(SavingsApiConstants.rejectedOnDateParamName).value(submittalDateAsString) - .failWithCodeNoParameterAddedToErrorCode("cannot.be.before.submittal.date"); - - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - if (DateUtils.isAfterBusinessDate(rejectedOn)) { - baseDataValidator.reset().parameter(SavingsApiConstants.rejectedOnDateParamName).value(rejectedOn) - .failWithCodeNoParameterAddedToErrorCode("cannot.be.a.future.date"); - - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - validateActivityNotBeforeClientOrGroupTransferDate(SavingsEvent.SAVINGS_APPLICATION_REJECTED, rejectedOn); - - return actualChanges; - } - - public Map applicantWithdrawsFromApplication(final AppUser currentUser, final JsonCommand command) { - final Map actualChanges = new LinkedHashMap<>(); - - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(SAVINGS_ACCOUNT_RESOURCE_NAME + SavingsApiConstants.withdrawnByApplicantAction); - - final SavingsAccountStatusType currentStatus = getStatus(); - if (!SavingsAccountStatusType.SUBMITTED_AND_PENDING_APPROVAL.hasStateOf(currentStatus)) { - - baseDataValidator.reset().parameter(SavingsApiConstants.withdrawnOnDateParamName) - .failWithCodeNoParameterAddedToErrorCode("not.in.submittedandpendingapproval.state"); - - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - this.status = SavingsAccountStatusType.WITHDRAWN_BY_APPLICANT.getValue(); - actualChanges.put(SavingsApiConstants.statusParamName, SavingsEnumerations.status(this.status)); - - final LocalDate withdrawnOn = command.localDateValueOfParameterNamed(SavingsApiConstants.withdrawnOnDateParamName); - final String withdrawnOnAsString = command.stringValueOfParameterNamed(SavingsApiConstants.withdrawnOnDateParamName); - - this.rejectedOnDate = null; - this.rejectedBy = null; - this.withdrawnOnDate = withdrawnOn; - this.withdrawnBy = currentUser; - this.closedOnDate = withdrawnOn; - this.closedBy = currentUser; - - actualChanges.put(SavingsApiConstants.localeParamName, command.locale()); - actualChanges.put(SavingsApiConstants.dateFormatParamName, command.dateFormat()); - actualChanges.put(SavingsApiConstants.withdrawnOnDateParamName, withdrawnOnAsString); - actualChanges.put(SavingsApiConstants.closedOnDateParamName, withdrawnOnAsString); - - final LocalDate submittalDate = getSubmittedOnDate(); - if (DateUtils.isBefore(withdrawnOn, submittalDate)) { - final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(command.extractLocale()); - final String submittalDateAsString = formatter.format(submittalDate); - - baseDataValidator.reset().parameter(SavingsApiConstants.withdrawnOnDateParamName).value(submittalDateAsString) - .failWithCodeNoParameterAddedToErrorCode("cannot.be.before.submittal.date"); - - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - if (DateUtils.isAfterBusinessDate(withdrawnOn)) { - baseDataValidator.reset().parameter(SavingsApiConstants.withdrawnOnDateParamName).value(withdrawnOn) - .failWithCodeNoParameterAddedToErrorCode("cannot.be.a.future.date"); - - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - validateActivityNotBeforeClientOrGroupTransferDate(SavingsEvent.SAVINGS_APPLICATION_WITHDRAWAL_BY_CUSTOMER, withdrawnOn); - - return actualChanges; - } - - public Map activate(final AppUser currentUser, final JsonCommand command) { - final Map actualChanges = new LinkedHashMap<>(); - - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(depositAccountType().resourceName() + SavingsApiConstants.activateAction); - - final SavingsAccountStatusType currentStatus = getStatus(); - if (!SavingsAccountStatusType.APPROVED.hasStateOf(currentStatus)) { - - baseDataValidator.reset().parameter(SavingsApiConstants.activatedOnDateParamName) - .failWithCodeNoParameterAddedToErrorCode("not.in.approved.state"); - - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - final Locale locale = command.extractLocale(); - final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); - final LocalDate activationDate = command.localDateValueOfParameterNamed(SavingsApiConstants.activatedOnDateParamName); - - this.status = SavingsAccountStatusType.ACTIVE.getValue(); - actualChanges.put(SavingsApiConstants.statusParamName, SavingsEnumerations.status(this.status)); - actualChanges.put(SavingsApiConstants.localeParamName, command.locale()); - actualChanges.put(SavingsApiConstants.dateFormatParamName, command.dateFormat()); - actualChanges.put(SavingsApiConstants.activatedOnDateParamName, activationDate.format(fmt)); - - this.rejectedOnDate = null; - this.rejectedBy = null; - this.withdrawnOnDate = null; - this.withdrawnBy = null; - this.closedOnDate = null; - this.closedBy = null; - this.activatedOnDate = activationDate; - this.activatedBy = currentUser; - this.lockedInUntilDate = calculateDateAccountIsLockedUntil(getActivationDate()); - - /* - * if (annualFeeSettingsSet()) { updateToNextAnnualFeeDueDateFrom(getActivationLocalDate()); } - */ - if (this.client != null && this.client.isActivatedAfter(activationDate)) { - final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(command.extractLocale()); - final String dateAsString = formatter.format(this.client.getActivationDate()); - baseDataValidator.reset().parameter(SavingsApiConstants.activatedOnDateParamName).value(dateAsString) - .failWithCodeNoParameterAddedToErrorCode("cannot.be.before.client.activation.date"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - if (this.group != null && this.group.isActivatedAfter(activationDate)) { - final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(command.extractLocale()); - final String dateAsString = formatter.format(this.client.getActivationDate()); - baseDataValidator.reset().parameter(SavingsApiConstants.activatedOnDateParamName).value(dateAsString) - .failWithCodeNoParameterAddedToErrorCode("cannot.be.before.group.activation.date"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - final LocalDate approvalDate = getApprovedOnDate(); - if (DateUtils.isBefore(activationDate, approvalDate)) { - final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(command.extractLocale()); - final String dateAsString = formatter.format(approvalDate); - - baseDataValidator.reset().parameter(SavingsApiConstants.activatedOnDateParamName).value(dateAsString) - .failWithCodeNoParameterAddedToErrorCode("cannot.be.before.approval.date"); - - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - if (DateUtils.isAfterBusinessDate(activationDate)) { - baseDataValidator.reset().parameter(SavingsApiConstants.activatedOnDateParamName).value(activationDate) - .failWithCodeNoParameterAddedToErrorCode("cannot.be.a.future.date"); - - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - validateActivityNotBeforeClientOrGroupTransferDate(SavingsEvent.SAVINGS_ACTIVATE, activationDate); - - return actualChanges; - } - - public void processAccountUponActivation(final boolean isSavingsInterestPostingAtCurrentPeriodEnd, - final Integer financialYearBeginningMonth) { - // update annual fee due date - for (SavingsAccountCharge charge : this.charges()) { - charge.updateToNextDueDateFrom(getActivationDate()); - } - - // auto pay the activation time charges (No need of checking the pivot date config) - this.payActivationCharges(isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, false); - // TODO : AA add activation charges to actual changes list - } - - public Money activateWithBalance() { - return Money.of(this.currency, this.minRequiredOpeningBalance); - } - - public void approveAndActivateApplication(final LocalDate appliedonDate, final AppUser appliedBy) { - this.status = SavingsAccountStatusType.ACTIVE.getValue(); - this.approvedOnDate = appliedonDate; - this.approvedBy = appliedBy; - this.rejectedOnDate = null; - this.rejectedBy = null; - this.withdrawnOnDate = null; - this.withdrawnBy = null; - this.closedOnDate = null; - this.closedBy = null; - this.activatedOnDate = appliedonDate; - this.activatedBy = appliedBy; - this.lockedInUntilDate = calculateDateAccountIsLockedUntil(getActivationDate()); - } - - private void payActivationCharges(final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth, - final boolean backdatedTxnsAllowedTill) { - boolean isSavingsChargeApplied = false; - boolean postReversals = false; - UUID refNo = UUID.randomUUID(); - for (SavingsAccountCharge savingsAccountCharge : this.charges()) { - if (savingsAccountCharge.isSavingsActivation()) { - isSavingsChargeApplied = true; - payCharge(savingsAccountCharge, savingsAccountCharge.getAmountOutstanding(getCurrency()), getActivationDate(), - backdatedTxnsAllowedTill, refNo.toString()); - } - } - - if (isSavingsChargeApplied) { - final MathContext mc = MathContext.DECIMAL64; - boolean isInterestTransfer = false; - LocalDate postInterestAsOnDate = null; - if (this.isBeforeLastPostingPeriod(getActivationDate(), backdatedTxnsAllowedTill)) { - final LocalDate today = DateUtils.getBusinessLocalDate(); - this.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, - postInterestAsOnDate, backdatedTxnsAllowedTill, postReversals); - } else { - final LocalDate today = DateUtils.getBusinessLocalDate(); - this.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestAsOnDate, backdatedTxnsAllowedTill, postReversals); - } - } - } - - public Map close(final AppUser currentUser, final JsonCommand command) { - final Map actualChanges = new LinkedHashMap<>(); - - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(SAVINGS_ACCOUNT_RESOURCE_NAME + SavingsApiConstants.closeAction); - - final SavingsAccountStatusType currentStatus = getStatus(); - 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 (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); - } - } - } - if (getAccountBalance().doubleValue() != 0) { - baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("results.in.balance.not.zero"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - validateActivityNotBeforeClientOrGroupTransferDate(SavingsEvent.SAVINGS_CLOSE_ACCOUNT, closedDate); - this.status = SavingsAccountStatusType.CLOSED.getValue(); - actualChanges.put(SavingsApiConstants.statusParamName, SavingsEnumerations.status(this.status)); - actualChanges.put(SavingsApiConstants.localeParamName, command.locale()); - actualChanges.put(SavingsApiConstants.dateFormatParamName, command.dateFormat()); - actualChanges.put(SavingsApiConstants.closedOnDateParamName, closedDate.format(fmt)); - - this.rejectedOnDate = null; - this.rejectedBy = null; - this.withdrawnOnDate = null; - this.withdrawnBy = null; - this.closedOnDate = closedDate; - this.closedBy = currentUser; - - return actualChanges; - } - - protected void validateActivityNotBeforeClientOrGroupTransferDate(final SavingsEvent event, final LocalDate activityDate) { - if (this.client != null) { - final LocalDate clientOfficeJoiningDate = this.client.getOfficeJoiningDate(); - if (DateUtils.isBefore(activityDate, clientOfficeJoiningDate)) { - throw new SavingsActivityPriorToClientTransferException(event.toString(), clientOfficeJoiningDate); - } - } - } - - private void validateAttemptToUndoTransferRelatedTransactions(final SavingsAccountTransaction savingsAccountTransaction) { - if (savingsAccountTransaction.isTransferRelatedTransaction()) { - throw new SavingsTransferTransactionsCannotBeUndoneException(savingsAccountTransaction.getId()); - } - } - - private LocalDate calculateDateAccountIsLockedUntil(final LocalDate activationLocalDate) { - - LocalDate lockedInUntilLocalDate = null; - final PeriodFrequencyType lockinPeriodFrequencyType = PeriodFrequencyType.fromInt(this.lockinPeriodFrequencyType); - switch (lockinPeriodFrequencyType) { - case INVALID: - break; - case DAYS: - lockedInUntilLocalDate = activationLocalDate.plusDays(this.lockinPeriodFrequency); - break; - case WEEKS: - lockedInUntilLocalDate = activationLocalDate.plusWeeks(this.lockinPeriodFrequency); - break; - case MONTHS: - lockedInUntilLocalDate = activationLocalDate.plusMonths(this.lockinPeriodFrequency); - break; - case YEARS: - lockedInUntilLocalDate = activationLocalDate.plusYears(this.lockinPeriodFrequency); - break; - case WHOLE_TERM: - LOG.error("TODO Implement calculateDateAccountIsLockedUntil for WHOLE_TERM"); - break; - } - - return lockedInUntilLocalDate; - } - - public Group group() { - return this.group; - } - - public boolean isWithdrawalFeeApplicableForTransfer() { - return this.withdrawalFeeApplicableForTransfer; - } - - public void activateAccountBasedOnBalance() { - if (getStatus().isClosed() && !this.summary.getAccountBalance(getCurrency()).isZero()) { - this.status = SavingsAccountStatusType.ACTIVE.getValue(); - } - } - - public LocalDate getClosedOnDate() { - return this.closedOnDate; - } - - public AppUser getClosedBy() { - return this.closedBy; - } - - public SavingsAccountSummary getSummary() { - return this.summary; - } - - public List getTransactions() { - return this.transactions; - } - - public void addTransaction(final SavingsAccountTransaction transaction) { - this.transactions.add(transaction); - } - - public void addTransactionToExisting(final SavingsAccountTransaction transaction) { - this.savingsAccountTransactions.add(transaction); - } - - public void setStatus(final Integer status) { - this.status = status; - } - - private Set associateChargesWithThisSavingsAccount(final Set savingsAccountCharges) { - for (final SavingsAccountCharge savingsAccountCharge : savingsAccountCharges) { - savingsAccountCharge.update(this); - } - return savingsAccountCharges; - } - - public boolean update(final Set newSavingsAccountCharges) { - if (newSavingsAccountCharges == null) { - return false; - } - - if (this.charges == null) { - this.charges = new HashSet<>(); - } - this.charges.clear(); - this.charges.addAll(associateChargesWithThisSavingsAccount(newSavingsAccountCharges)); - return true; - } - - public boolean hasCurrencyCodeOf(final String matchingCurrencyCode) { - if (this.currency == null) { - return false; - } - return this.currency.getCode().equalsIgnoreCase(matchingCurrencyCode); - } - - public void removeCharge(final SavingsAccountCharge charge) { - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(SAVINGS_ACCOUNT_RESOURCE_NAME); - - if (isClosed()) { - baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("delete.transaction.invalid.account.is.closed"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - if (isActive() || isApproved()) { - baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("delete.transaction.invalid.account.is.active"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - this.charges.remove(charge); - } - - public void waiveCharge(final Long savingsAccountChargeId, final boolean backdatedTxnsAllowedTill) { - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(SAVINGS_ACCOUNT_RESOURCE_NAME); - - if (isClosed()) { - baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("transaction.invalid.account.is.closed"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - if (isNotActive()) { - baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("transaction.invalid.account.is.not.active"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - final SavingsAccountCharge savingsAccountCharge = getCharge(savingsAccountChargeId); - - if (savingsAccountCharge.isNotActive()) { - baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("charge.is.not.active"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - if (savingsAccountCharge.isWithdrawalFee()) { - baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("transaction.invalid.waiver.of.withdrawal.fee.not.supported"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - // validate charge is not already paid or waived - if (savingsAccountCharge.isWaived()) { - baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("transaction.invalid.account.charge.is.already.waived"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } else if (savingsAccountCharge.isPaid()) { - baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("transaction.invalid.account.charge.is.paid"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - // waive charge - final Money amountWaived = savingsAccountCharge.waive(getCurrency()); - handleWaiverChargeTransactions(savingsAccountCharge, amountWaived, backdatedTxnsAllowedTill); - } - - public void addCharge(final DateTimeFormatter formatter, final SavingsAccountCharge savingsAccountCharge, - final Charge chargeDefinition) { - - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(SAVINGS_ACCOUNT_RESOURCE_NAME); - - if (isClosed()) { - baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("transaction.invalid.account.is.closed"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - if (!hasCurrencyCodeOf(chargeDefinition.getCurrencyCode())) { - baseDataValidator.reset() - .failWithCodeNoParameterAddedToErrorCode("transaction.invalid.account.currency.and.charge.currency.not.same"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - final LocalDate chargeDueDate = savingsAccountCharge.getDueDate(); - - if (savingsAccountCharge.isOnSpecifiedDueDate()) { - if (DateUtils.isBefore(chargeDueDate, getActivationDate())) { - baseDataValidator.reset().parameter(dueAsOfDateParamName).value(getActivationDate().format(formatter)) - .failWithCodeNoParameterAddedToErrorCode("before.activationDate"); - throw new PlatformApiDataValidationException(dataValidationErrors); - } else if (DateUtils.isBefore(chargeDueDate, getSubmittedOnDate())) { - baseDataValidator.reset().parameter(dueAsOfDateParamName).value(getSubmittedOnDate().format(formatter)) - .failWithCodeNoParameterAddedToErrorCode("before.submittedOnDate"); - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - if (savingsAccountCharge.isSavingsActivation() && !(isSubmittedAndPendingApproval() || (isApproved() && isNotActive()))) { - baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("not.valid.account.status.cannot.add.activation.time.charge"); - throw new PlatformApiDataValidationException(dataValidationErrors); - } - - // Only one annual fee is supported per account - if (savingsAccountCharge.isAnnualFee()) { - if (this.isAnnualFeeExists()) { - baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("multiple.annual.fee.per.account.not.supported"); - throw new PlatformApiDataValidationException(dataValidationErrors); - } - - } - - if (savingsAccountCharge.isAnnualFee() || savingsAccountCharge.isMonthlyFee() || savingsAccountCharge.isWeeklyFee()) { - // update due date - if (isActive()) { - savingsAccountCharge.updateToNextDueDateFrom(getActivationDate()); - } else if (isApproved()) { - savingsAccountCharge.updateToNextDueDateFrom(getApprovedOnDate()); - } - } - - // activation charge and withdrawal charges not required this validation - if (savingsAccountCharge.isOnSpecifiedDueDate()) { - validateActivityNotBeforeClientOrGroupTransferDate(SavingsEvent.SAVINGS_APPLY_CHARGE, chargeDueDate); - } - - // add new charge to savings account - this.charges.add(savingsAccountCharge); - - } - - private boolean isAnnualFeeExists() { - for (SavingsAccountCharge charge : this.charges()) { - if (charge.isAnnualFee()) { - return true; - } - } - return false; - } - - public SavingsAccountTransaction payCharge(final SavingsAccountCharge savingsAccountCharge, final BigDecimal amountPaid, - final LocalDate transactionDate, final DateTimeFormatter formatter, final boolean backdatedTxnsAllowedTill, - final String refNo) { - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(SAVINGS_ACCOUNT_RESOURCE_NAME); - - if (isClosed()) { - baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("transaction.invalid.account.is.closed"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - if (isNotActive()) { - baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("transaction.invalid.account.is.not.active"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - if (savingsAccountCharge.isNotActive()) { - baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("charge.is.not.active"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - if (DateUtils.isBefore(transactionDate, getActivationDate())) { - baseDataValidator.reset().parameter(dueAsOfDateParamName).value(getActivationDate().format(formatter)) - .failWithCodeNoParameterAddedToErrorCode("transaction.before.activationDate"); - throw new PlatformApiDataValidationException(dataValidationErrors); - } - - if (DateUtils.isDateInTheFuture(transactionDate)) { - baseDataValidator.reset().parameter(dueAsOfDateParamName).value(transactionDate.format(formatter)) - .failWithCodeNoParameterAddedToErrorCode("transaction.is.futureDate"); - throw new PlatformApiDataValidationException(dataValidationErrors); - } - - if (savingsAccountCharge.isSavingsActivation()) { - baseDataValidator.reset() - .failWithCodeNoParameterAddedToErrorCode("transaction.not.valid.cannot.pay.activation.time.charge.is.automated"); - throw new PlatformApiDataValidationException(dataValidationErrors); - } - - if (savingsAccountCharge.isAnnualFee()) { - final LocalDate annualFeeDueDate = savingsAccountCharge.getDueDate(); - if (annualFeeDueDate == null) { - baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("no.annualfee.settings"); - throw new PlatformApiDataValidationException(dataValidationErrors); - } - - if (DateUtils.isBefore(transactionDate, annualFeeDueDate)) { - baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("transaction.before.dueDate"); - throw new PlatformApiDataValidationException(dataValidationErrors); - } - - LocalDate currentAnnualFeeNextDueDate = findLatestAnnualFeeTransactionDueDate(); - if (DateUtils.isEqual(currentAnnualFeeNextDueDate, transactionDate)) { - baseDataValidator.reset().parameter("dueDate").value(transactionDate.format(formatter)) - .failWithCodeNoParameterAddedToErrorCode("transaction.exists.on.date"); - - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - // validate charge is not already paid or waived - if (savingsAccountCharge.isWaived()) { - baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("transaction.invalid.account.charge.is.already.waived"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } else if (savingsAccountCharge.isPaid()) { - baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("transaction.invalid.account.charge.is.paid"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - final Money chargePaid = Money.of(currency, amountPaid); - if (!savingsAccountCharge.getAmountOutstanding(getCurrency()).isGreaterThanOrEqualTo(chargePaid)) { - baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("transaction.invalid.charge.amount.paid.in.access"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - return this.payCharge(savingsAccountCharge, chargePaid, transactionDate, backdatedTxnsAllowedTill, refNo); - } - - public SavingsAccountTransaction payCharge(final SavingsAccountCharge savingsAccountCharge, final Money amountPaid, - final LocalDate transactionDate, final boolean backdatedTxnsAllowedTill, String refNo) { - savingsAccountCharge.pay(getCurrency(), amountPaid); - return handlePayChargeTransactions(savingsAccountCharge, amountPaid, transactionDate, backdatedTxnsAllowedTill, refNo); - } - - private SavingsAccountTransaction handlePayChargeTransactions(SavingsAccountCharge savingsAccountCharge, Money transactionAmount, - final LocalDate transactionDate, final boolean backdatedTxnsAllowedTill, final String refNo) { - SavingsAccountTransaction chargeTransaction; - - if (savingsAccountCharge.isWithdrawalFee()) { - chargeTransaction = SavingsAccountTransaction.withdrawalFee(this, office(), transactionDate, transactionAmount, refNo); - } else if (savingsAccountCharge.isAnnualFee()) { - chargeTransaction = SavingsAccountTransaction.annualFee(this, office(), transactionDate, transactionAmount); - } else { - chargeTransaction = SavingsAccountTransaction.charge(this, office(), transactionDate, transactionAmount); - } - - handleChargeTransactions(savingsAccountCharge, chargeTransaction, backdatedTxnsAllowedTill); - return chargeTransaction; - } - - private void handleWaiverChargeTransactions(SavingsAccountCharge savingsAccountCharge, Money transactionAmount, - boolean backdatedTxnsAllowedTill) { - final SavingsAccountTransaction chargeTransaction = SavingsAccountTransaction.waiver(this, office(), - DateUtils.getBusinessLocalDate(), transactionAmount); - handleChargeTransactions(savingsAccountCharge, chargeTransaction, backdatedTxnsAllowedTill); - } - - private void handleChargeTransactions(final SavingsAccountCharge savingsAccountCharge, final SavingsAccountTransaction transaction, - final boolean backdatedTxnsAllowedTill) { - // Provide a link between transaction and savings charge for which - // amount is waived. - final SavingsAccountChargePaidBy chargePaidBy = SavingsAccountChargePaidBy.instance(transaction, savingsAccountCharge, - transaction.getAmount(this.currency).getAmount()); - transaction.getSavingsAccountChargesPaid().add(chargePaidBy); - if (backdatedTxnsAllowedTill) { - this.savingsAccountTransactions.add(transaction); - this.summary.updateSummaryWithPivotConfig(this.currency, this.savingsAccountTransactionSummaryWrapper, transaction, - this.savingsAccountTransactions); - } else { - this.transactions.add(transaction); - } - } - - private SavingsAccountCharge getCharge(final Long savingsAccountChargeId) { - SavingsAccountCharge charge = null; - for (final SavingsAccountCharge existingCharge : this.charges) { - if (existingCharge.getId().equals(savingsAccountChargeId)) { - charge = existingCharge; - break; - } - } - - if (charge == null) { - throw new SavingsAccountChargeNotFoundException(savingsAccountChargeId, getId()); - } - - return charge; - } - - public Set charges() { - return (this.charges == null) ? new HashSet() : this.charges; - } - - public void validateAccountValuesWithProduct() { - - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(SAVINGS_ACCOUNT_RESOURCE_NAME); - - if (this.overdraftLimit != null && this.product.overdraftLimit() != null - && this.overdraftLimit.compareTo(this.product.overdraftLimit()) > 0) { - baseDataValidator.reset().parameter(SavingsApiConstants.overdraftLimitParamName).value(this.overdraftLimit) - .failWithCode("cannot.exceed.product.value"); - } - - validateInterestPostingAndCompoundingPeriodTypes(baseDataValidator); - - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - public boolean allowOverdraft() { - return this.allowOverdraft; - } - - public LocalDate accountSubmittedOrActivationDate() { - return getActivationDate() == null ? getSubmittedOnDate() : getActivationDate(); - } - - protected boolean isTransferInterestToOtherAccount() { - return false; - } - - public boolean accountSubmittedAndActivationOnSameDate() { - return getActivationDate() != null && DateUtils.isEqual(getActivationDate(), getSubmittedOnDate()); - - } - - public void validateInterestPostingAndCompoundingPeriodTypes(final DataValidatorBuilder baseDataValidator) { - Map> postingtoCompoundMap = new HashMap<>(); - - postingtoCompoundMap.put(SavingsPostingInterestPeriodType.DAILY, Arrays.asList(SavingsCompoundingInterestPeriodType.DAILY)); - - postingtoCompoundMap.put(SavingsPostingInterestPeriodType.MONTHLY, - Arrays.asList(SavingsCompoundingInterestPeriodType.DAILY, SavingsCompoundingInterestPeriodType.MONTHLY)); - - postingtoCompoundMap.put(SavingsPostingInterestPeriodType.QUATERLY, Arrays.asList(SavingsCompoundingInterestPeriodType.DAILY, - SavingsCompoundingInterestPeriodType.MONTHLY, SavingsCompoundingInterestPeriodType.QUATERLY)); - - postingtoCompoundMap.put(SavingsPostingInterestPeriodType.BIANNUAL, - Arrays.asList(SavingsCompoundingInterestPeriodType.DAILY, SavingsCompoundingInterestPeriodType.MONTHLY, - SavingsCompoundingInterestPeriodType.QUATERLY, SavingsCompoundingInterestPeriodType.BI_ANNUAL)); - - postingtoCompoundMap.put(SavingsPostingInterestPeriodType.ANNUAL, - Arrays.asList(SavingsCompoundingInterestPeriodType.DAILY, SavingsCompoundingInterestPeriodType.MONTHLY, - SavingsCompoundingInterestPeriodType.QUATERLY, SavingsCompoundingInterestPeriodType.BI_ANNUAL, - SavingsCompoundingInterestPeriodType.ANNUAL)); - - SavingsPostingInterestPeriodType savingsPostingInterestPeriodType = SavingsPostingInterestPeriodType - .fromInt(interestPostingPeriodType); - SavingsCompoundingInterestPeriodType savingsCompoundingInterestPeriodType = SavingsCompoundingInterestPeriodType - .fromInt(interestCompoundingPeriodType); - - if (postingtoCompoundMap.get(savingsPostingInterestPeriodType) == null) { - baseDataValidator.failWithCodeNoParameterAddedToErrorCode("posting.period.type.is.less.than.compound.period.type", - savingsPostingInterestPeriodType.name(), savingsCompoundingInterestPeriodType.name()); - - } - } - - public AppUser getSubmittedBy() { - return this.submittedBy; - } - - public AppUser getApprovedBy() { - return this.approvedBy; - } - - public boolean allowDeposit() { - return true; - } - - public boolean allowWithdrawal() { - return true; - } - - public boolean allowModify() { - return true; - } - - public boolean isTransactionsAllowed() { - return isActive(); - } - - public boolean isTransactionAllowed(SavingsAccountTransactionType transactionType, LocalDate transactionDate) { - if (!isTransactionsAllowed()) { - return false; - } - - Client client = getClient(); - if (client != null && !client.isActive()) { - return false; - } - Group group = group(); - if (group != null && !group.isActive()) { - return false; - } - - if (transactionDate == null) { - return true; - } - if (DateUtils.isDateInTheFuture(transactionDate) || DateUtils.isBefore(transactionDate, getActivationDate())) { - return false; - } - if (transactionType.isCredit()) { - return true; - } - return !isAccountLocked(transactionDate); - } - - public BigDecimal minBalanceForInterestCalculation() { - return this.minBalanceForInterestCalculation; - } - - public void inactivateCharge(SavingsAccountCharge savingsAccountCharge, LocalDate inactivationOnDate) { - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(SAVINGS_ACCOUNT_RESOURCE_NAME); - - if (isClosed()) { - baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("transaction.invalid.account.is.closed"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - if (isNotActive()) { - baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("transaction.invalid.account.is.not.active"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - savingsAccountCharge.inactiavateCharge(inactivationOnDate); - } - - public SavingsAccountCharge getUpdatedChargeDetails(SavingsAccountCharge savingsAccountCharge) { - for (final SavingsAccountCharge charge : this.charges) { - if (charge.equals(savingsAccountCharge)) { - savingsAccountCharge = charge; - break; - } - } - return savingsAccountCharge; - } - - public String getAccountNumber() { - return this.accountNumber; - } - - private Money minRequiredBalanceDerived(final MonetaryCurrency currency) { - Money minReqBalance = Money.zero(currency); - if (this.enforceMinRequiredBalance) { - minReqBalance = minReqBalance.plus(this.minRequiredBalance); - } - if (this.allowOverdraft) { - minReqBalance = minReqBalance.minus(this.overdraftLimit); - } - return minReqBalance; - } - - public BigDecimal getOnHoldFunds() { - return this.onHoldFunds == null ? BigDecimal.ZERO : this.onHoldFunds; - } - - public void holdFunds(BigDecimal onHoldFunds) { - this.onHoldFunds = getOnHoldFunds().add(onHoldFunds); - } - - public void releaseFunds(BigDecimal onHoldFunds) { - this.onHoldFunds = getOnHoldFunds().subtract(onHoldFunds); - } - - public BigDecimal getWithdrawableBalance() { - return getAccountBalance().subtract(minRequiredBalanceDerived(getCurrency()).getAmount()).subtract(this.getOnHoldFunds()) - .subtract(this.getSavingsHoldAmount()); - } - - public BigDecimal getWithdrawableBalanceWithLien() { - return getAccountBalance().subtract(minRequiredBalanceDerived(getCurrency()).getAmount()).subtract(this.getOnHoldFunds()) - .subtract(this.getSavingsHoldAmount()); - } - - public BigDecimal getWithdrawableBalanceWithoutMinimumBalance() { - return getAccountBalance().subtract(this.getOnHoldFunds()).subtract(this.getSavingsHoldAmount()); - } - - public TaxGroup getTaxGroup() { - return this.taxGroup; - } - - public boolean withHoldTax() { - return this.withHoldTax; - } - - public void setWithHoldTax(boolean withHoldTax) { - this.withHoldTax = withHoldTax; - } - - protected boolean applyWithholdTaxForDepositAccounts(final LocalDate interestPostingUpToDate, boolean recalucateDailyBalance, - final boolean backdatedTxnsAllowedTill) { - final List withholdTransactions = findWithHoldTransactions(); - SavingsAccountTransaction withholdTransaction = findTransactionFor(interestPostingUpToDate, withholdTransactions); - final BigDecimal totalInterestPosted = this.savingsAccountTransactionSummaryWrapper.calculateTotalInterestPosted(this.currency, - this.transactions); - if (withholdTransaction == null && this.withHoldTax()) { - boolean isWithholdTaxAdded = createWithHoldTransaction(totalInterestPosted, interestPostingUpToDate, backdatedTxnsAllowedTill); - recalucateDailyBalance = recalucateDailyBalance || isWithholdTaxAdded; - } else { - boolean isWithholdTaxAdded = updateWithHoldTransaction(totalInterestPosted, withholdTransaction); - recalucateDailyBalance = recalucateDailyBalance || isWithholdTaxAdded; - } - - return recalucateDailyBalance; - } - - public void setSubStatusInactive(final boolean backdatedTxnsAllowedTill) { - this.sub_status = SavingsAccountSubStatusEnum.INACTIVE.getValue(); - LocalDate transactionDate = DateUtils.getBusinessLocalDate(); - for (SavingsAccountCharge charge : this.charges()) { - if (charge.isSavingsNoActivity() && charge.isActive()) { - charge.updateWithdralFeeAmount(this.getAccountBalance()); - UUID refNo = UUID.randomUUID(); - this.payCharge(charge, charge.getAmountOutstanding(this.getCurrency()), transactionDate, backdatedTxnsAllowedTill, - refNo.toString()); - } - } - boolean postReversals = false; - recalculateDailyBalances(Money.zero(this.currency), transactionDate, backdatedTxnsAllowedTill, postReversals); - this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, this.transactions); - } - - public void recalculateBalancesAfterFacilityCharges(final LocalDate transactionDate) { - recalculateDailyBalances(Money.zero(this.currency), transactionDate, false, false); - this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, this.transactions); - } - - public void setSubStatusDormant() { - this.sub_status = SavingsAccountSubStatusEnum.DORMANT.getValue(); - } - - public void escheat(AppUser appUser) { - this.status = SavingsAccountStatusType.CLOSED.getValue(); - this.sub_status = SavingsAccountSubStatusEnum.ESCHEAT.getValue(); - this.closedOnDate = DateUtils.getBusinessLocalDate(); - this.closedBy = appUser; - boolean postInterestAsOnDate = false; - boolean postReversals = false; - LocalDate transactionDate = DateUtils.getBusinessLocalDate(); - if (this.getSummary().getAccountBalance(this.getCurrency()).isGreaterThanZero()) { - SavingsAccountTransaction transaction = SavingsAccountTransaction.escheat(this, transactionDate, postInterestAsOnDate); - this.transactions.add(transaction); - } - recalculateDailyBalances(Money.zero(this.currency), transactionDate, false, postReversals); - this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, this.transactions); - } - - public void loadLazyCollections() { - transactions.size(); - charges.size(); - savingsOfficerHistory.size(); - if (group != null) { - Office dummyOffice = group.getOffice(); - } // Ensure lazy loading of group if set - } - - public void updateSavingsAccountSummary(final List transactions) { - this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, transactions); - } - - public void updateReason(final String reasonForBlock) { - this.reasonForBlock = reasonForBlock; - } - - public Map block() { - - final Map actualChanges = new LinkedHashMap<>(); - - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(depositAccountType().resourceName() + SavingsApiConstants.blockAction); - - final SavingsAccountStatusType currentStatus = getStatus(); - if (!SavingsAccountStatusType.ACTIVE.hasStateOf(currentStatus)) { - - baseDataValidator.reset().parameter(SavingsApiConstants.statusParamName) - .failWithCodeNoParameterAddedToErrorCode(SavingsApiConstants.ERROR_MSG_SAVINGS_ACCOUNT_NOT_ACTIVE); - - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - this.sub_status = SavingsAccountSubStatusEnum.BLOCK.getValue(); - actualChanges.put(SavingsApiConstants.subStatusParamName, SavingsEnumerations.subStatus(this.sub_status)); - - return actualChanges; - } - - public Map unblock() { - - final Map actualChanges = new LinkedHashMap<>(); - - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(depositAccountType().resourceName() + SavingsApiConstants.unblockAction); - - final SavingsAccountStatusType currentStatus = getStatus(); - if (!SavingsAccountStatusType.ACTIVE.hasStateOf(currentStatus)) { - - baseDataValidator.reset().parameter(SavingsApiConstants.statusParamName) - .failWithCodeNoParameterAddedToErrorCode(SavingsApiConstants.ERROR_MSG_SAVINGS_ACCOUNT_NOT_ACTIVE); - - } - - final SavingsAccountSubStatusEnum currentSubStatus = SavingsAccountSubStatusEnum.fromInt(this.sub_status); - if (!SavingsAccountSubStatusEnum.BLOCK.hasStateOf(currentSubStatus)) { - baseDataValidator.reset().parameter(SavingsApiConstants.subStatusParamName) - .failWithCodeNoParameterAddedToErrorCode("not.in.blocked.state"); - } - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - this.sub_status = SavingsAccountSubStatusEnum.NONE.getValue(); - actualChanges.put(SavingsApiConstants.subStatusParamName, SavingsEnumerations.subStatus(this.sub_status)); - return actualChanges; - } - - public Map blockCredits(Integer currentSubstatus) { - - final Map actualChanges = new LinkedHashMap<>(); - - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(depositAccountType().resourceName() + SavingsApiConstants.blockCreditsAction); - - final SavingsAccountStatusType currentStatus = getStatus(); - if (!SavingsAccountStatusType.ACTIVE.hasStateOf(currentStatus)) { - - baseDataValidator.reset().parameter(SavingsApiConstants.statusParamName) - .failWithCodeNoParameterAddedToErrorCode(SavingsApiConstants.ERROR_MSG_SAVINGS_ACCOUNT_NOT_ACTIVE); - } - if (SavingsAccountSubStatusEnum.BLOCK.hasStateOf(SavingsAccountSubStatusEnum.fromInt(currentSubstatus))) { - - baseDataValidator.reset().parameter(SavingsApiConstants.subStatusParamName) - .value(SavingsAccountSubStatusEnum.fromInt(currentSubstatus)).failWithCodeNoParameterAddedToErrorCode("currently.set"); - } - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - if (SavingsAccountSubStatusEnum.BLOCK_DEBIT.hasStateOf(SavingsAccountSubStatusEnum.fromInt(currentSubstatus))) { - this.sub_status = SavingsAccountSubStatusEnum.BLOCK.getValue(); - } else { - this.sub_status = SavingsAccountSubStatusEnum.BLOCK_CREDIT.getValue(); - } - actualChanges.put(SavingsApiConstants.subStatusParamName, SavingsEnumerations.subStatus(this.sub_status)); - - return actualChanges; - } - - public Map unblockCredits() { - - final Map actualChanges = new LinkedHashMap<>(); - - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(depositAccountType().resourceName() + SavingsApiConstants.unblockCreditsAction); - - final SavingsAccountStatusType currentStatus = getStatus(); - if (!SavingsAccountStatusType.ACTIVE.hasStateOf(currentStatus)) { - baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode(SavingsApiConstants.ERROR_MSG_SAVINGS_ACCOUNT_NOT_ACTIVE); - } - - final SavingsAccountSubStatusEnum currentSubStatus = SavingsAccountSubStatusEnum.fromInt(this.sub_status); - if (!(SavingsAccountSubStatusEnum.BLOCK_CREDIT.hasStateOf(currentSubStatus) - || SavingsAccountSubStatusEnum.BLOCK.hasStateOf(currentSubStatus))) { - baseDataValidator.reset().parameter(SavingsApiConstants.statusParamName) - .failWithCodeNoParameterAddedToErrorCode("credits.are.not.blocked"); - } - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - if (SavingsAccountSubStatusEnum.BLOCK.hasStateOf(currentSubStatus)) { - this.sub_status = SavingsAccountSubStatusEnum.BLOCK_DEBIT.getValue(); - } else { - this.sub_status = SavingsAccountSubStatusEnum.NONE.getValue(); - } - actualChanges.put(SavingsApiConstants.subStatusParamName, SavingsEnumerations.subStatus(this.sub_status)); - return actualChanges; - } - - public Map blockDebits(Integer currentSubstatus) { - - final Map actualChanges = new LinkedHashMap<>(); - - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(depositAccountType().resourceName() + SavingsApiConstants.blockDebitsAction); - - final SavingsAccountStatusType currentStatus = getStatus(); - if (!SavingsAccountStatusType.ACTIVE.hasStateOf(currentStatus)) { - baseDataValidator.reset().parameter(SavingsApiConstants.statusParamName) - .failWithCodeNoParameterAddedToErrorCode(SavingsApiConstants.ERROR_MSG_SAVINGS_ACCOUNT_NOT_ACTIVE); - - } - if (SavingsAccountSubStatusEnum.BLOCK.hasStateOf(SavingsAccountSubStatusEnum.fromInt(currentSubstatus))) { - - baseDataValidator.reset().parameter(SavingsApiConstants.subStatusParamName) - .value(SavingsAccountSubStatusEnum.fromInt(currentSubstatus)).failWithCodeNoParameterAddedToErrorCode("currently.set"); - } - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - if (SavingsAccountSubStatusEnum.BLOCK_CREDIT.hasStateOf(SavingsAccountSubStatusEnum.fromInt(currentSubstatus))) { - this.sub_status = SavingsAccountSubStatusEnum.BLOCK.getValue(); - } else { - this.sub_status = SavingsAccountSubStatusEnum.BLOCK_DEBIT.getValue(); - } - actualChanges.put(SavingsApiConstants.subStatusParamName, SavingsEnumerations.subStatus(this.sub_status)); - - return actualChanges; - } - - public Map unblockDebits() { - - final Map actualChanges = new LinkedHashMap<>(); - - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(depositAccountType().resourceName() + SavingsApiConstants.unblockDebitsAction); - - final SavingsAccountStatusType currentStatus = getStatus(); - if (!SavingsAccountStatusType.ACTIVE.hasStateOf(currentStatus)) { - - baseDataValidator.reset().parameter(SavingsApiConstants.statusParamName) - .failWithCodeNoParameterAddedToErrorCode(SavingsApiConstants.ERROR_MSG_SAVINGS_ACCOUNT_NOT_ACTIVE); - - } - - final SavingsAccountSubStatusEnum currentSubStatus = SavingsAccountSubStatusEnum.fromInt(this.sub_status); - if (!(SavingsAccountSubStatusEnum.BLOCK_DEBIT.hasStateOf(currentSubStatus) - || SavingsAccountSubStatusEnum.BLOCK.hasStateOf(currentSubStatus))) { - baseDataValidator.reset().parameter(SavingsApiConstants.subStatusParamName) - .failWithCodeNoParameterAddedToErrorCode("debits.are.not.blocked"); - } - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - if (SavingsAccountSubStatusEnum.BLOCK.hasStateOf(currentSubStatus)) { - this.sub_status = SavingsAccountSubStatusEnum.BLOCK_CREDIT.getValue(); - } else { - this.sub_status = SavingsAccountSubStatusEnum.NONE.getValue(); - } - actualChanges.put(SavingsApiConstants.subStatusParamName, SavingsEnumerations.subStatus(this.sub_status)); - return actualChanges; - } - - public SavingsAccountStatusType getStatus() { - return SavingsAccountStatusType.fromInt(status); - } - - public Integer getSubStatus() { - return this.sub_status; - } - - public void validateForAccountBlock() { - final SavingsAccountSubStatusEnum currentSubStatus = SavingsAccountSubStatusEnum.fromInt(this.getSubStatus()); - if (SavingsAccountSubStatusEnum.BLOCK.hasStateOf(currentSubStatus)) { - throw new SavingsAccountBlockedException(this.getId()); - } - } - - public void validateForDebitBlock() { - final SavingsAccountSubStatusEnum currentSubStatus = SavingsAccountSubStatusEnum.fromInt(this.getSubStatus()); - if (SavingsAccountSubStatusEnum.BLOCK_DEBIT.hasStateOf(currentSubStatus)) { - throw new SavingsAccountDebitsBlockedException(this.getId()); - } - } - - public void validateForCreditBlock() { - final SavingsAccountSubStatusEnum currentSubStatus = SavingsAccountSubStatusEnum.fromInt(this.getSubStatus()); - if (SavingsAccountSubStatusEnum.BLOCK_CREDIT.hasStateOf(currentSubStatus)) { - throw new SavingsAccountCreditsBlockedException(this.getId()); - } - } - - public LocalDate retrieveLastTransactionDate() { - final List transactionsSortedByDate = retrieveListOfTransactions(); - SavingsAccountTransaction lastTransaction = null; - if (transactionsSortedByDate.size() > 0) { - lastTransaction = transactionsSortedByDate.get(transactionsSortedByDate.size() - 1); - } - LocalDate lastransactionDate = null; - if (lastTransaction != null) { - lastransactionDate = lastTransaction.getTransactionDate(); - } - return lastransactionDate; - } - - public LocalDate retrieveLastTransactionDateWithPivotConfig() { - final List transactionsSortedByDate = retrieveSortedTransactions(); - SavingsAccountTransaction lastTransaction = null; - if (transactionsSortedByDate.size() > 0) { - lastTransaction = transactionsSortedByDate.get(transactionsSortedByDate.size() - 1); - } - LocalDate lastransactionDate = null; - if (lastTransaction != null) { - lastransactionDate = lastTransaction.getTransactionDate(); - } - return lastransactionDate; - } - - public BigDecimal getSavingsHoldAmount() { - return this.savingsOnHoldAmount == null ? BigDecimal.ZERO : this.savingsOnHoldAmount; - } - - public void holdAmount(BigDecimal amount) { - this.savingsOnHoldAmount = getSavingsHoldAmount().add(amount); - } - - public void releaseOnHoldAmount(BigDecimal amount) { - this.savingsOnHoldAmount = getSavingsHoldAmount().subtract(amount); - } - - public AccountType getAccountType() { - return AccountType.fromInt(accountType); - } - - public Integer getAccountTypes() { - return accountType; - } - - public void setAccountType(Integer accountType) { - this.accountType = accountType; - } - - private boolean isOverdraft() { - return allowOverdraft; - } - - public Long getGroupId() { - return this.groupId(); - } - - public Integer getInterestCalculationDaysInYearType() { - return this.interestCalculationDaysInYearType; - } - - public BigDecimal getMinRequiredOpeningBalance() { - return this.minRequiredOpeningBalance; - } - - public Integer getLockinPeriodFrequency() { - return this.lockinPeriodFrequency; - } - - public Integer getLockinPeriodFrequencyType() { - return this.lockinPeriodFrequencyType; - } - - public boolean isWithdrawalFeeForTransfer() { - return this.withdrawalFeeApplicableForTransfer; - } - - public boolean isAllowOverdraft() { - return this.allowOverdraft; - } - - public BigDecimal getOverdraftLimit() { - return this.overdraftLimit; - } - - public BigDecimal getMinOverdraftForInterestCalculation() { - return this.minOverdraftForInterestCalculation; - } - - public BigDecimal getMinRequiredBalance() { - return this.minRequiredBalance; - } - - public boolean isEnforceMinRequiredBalance() { - return this.enforceMinRequiredBalance; - } - - public BigDecimal getMaxAllowedLienLimit() { - return this.maxAllowedLienLimit; - } - - public boolean isLienAllowed() { - return this.lienAllowed; - } - - public BigDecimal getMinBalanceForInterestCalculation() { - return this.minBalanceForInterestCalculation; - } - - public int getVersion() { - return this.version; - } - - public boolean isWithHoldTax() { - return this.withHoldTax; - } - - public void setAccruedTillDate(LocalDate accruedTillDate) { - this.accruedTillDate = accruedTillDate; - } - - public LocalDate getLastClosedBusinessDate() { - return this.lastClosedBusinessDate; - } - - public void setLastClosedBusinessDate(LocalDate lastClosedBusinessDate) { - this.lastClosedBusinessDate = lastClosedBusinessDate; - } - - public List toSavingsAccountTransactionDetailsForPostingPeriodList( - List transactions) { - return transactions.stream() - .map(transaction -> transaction.toSavingsAccountTransactionDetailsForPostingPeriod(this.currency, this.allowOverdraft)) - .toList(); - } - - public void accrualsForSavingsReverse(SavingsAccountTransactionDTO transactionDTO, final boolean backdatedTxnsAllowedTill) { - List accountTransactionsSorted = null; - - if (backdatedTxnsAllowedTill) { - accountTransactionsSorted = retrieveSortedTransactions(); - } else { - accountTransactionsSorted = retrieveListOfTransactions(); - } - for (final SavingsAccountTransaction transaction : accountTransactionsSorted) { - boolean typeTransaccionValidation = transaction.getTransactionType() == SavingsAccountTransactionType.ACCRUAL; - if (typeTransaccionValidation && (transaction.getDateOf().isAfter(transactionDTO.getTransactionDate()) - || transaction.getDateOf().isEqual(transactionDTO.getTransactionDate()))) { - transaction.reverse(); - } - } - } - - public List toSavingsAccountTransactionDetailsForPostingPeriodList() { - return retreiveOrderedNonInterestPostingTransactions().stream() - .map(transaction -> transaction.toSavingsAccountTransactionDetailsForPostingPeriod(this.currency, this.allowOverdraft)) - .toList(); - } - - @Override - public DepositAccountType depositAccountType() { - return DepositAccountType.fromInt(100); - } - - // ─── CBS extended fields ─────────────────────────────────────────────────── - - @Column(name = "cbs_account_type", length = 100) - private String cbsAccountType; - - @Column(name = "customer_segment", length = 100) - private String customerSegment; - - @Column(name = "account_status", length = 50) - private String accountStatus; - - @Column(name = "mode_of_operation", length = 50) - private String modeOfOperation; - - @Column(name = "special_category", length = 100) - private String specialCategory; - - @Column(name = "weaker_section", length = 100) - private String weakerSection; - - @Column(name = "guardian_name", length = 200) - private String guardianName; - - @Column(name = "guardian_cif_number", length = 50) - private String guardianCifNumber; - - @Column(name = "guardian_relationship", length = 100) - private String guardianRelationship; - - @Column(name = "minor_date_of_birth") - private LocalDate minorDateOfBirth; - - @Column(name = "guardian_client_id") - private Long guardianClientId; - - @Column(name = "introducer_client_id") - private Long introducerClientId; - - @Column(name = "introducer_name", length = 200) - private String introducerName; - - @Column(name = "introducer_cif_number", length = 50) - private String introducerCifNumber; - - @Column(name = "nominee_data", columnDefinition = "TEXT") - private String nomineeData; - - @Column(name = "linked_account_id") - private Long linkedAccountId; - - @Column(name = "linked_account_type", length = 20) - private String linkedAccountType; - - @Column(name = "linked_account_no", length = 50) - private String linkedAccountNo; - - @Column(name = "nomination_facility", nullable = false, columnDefinition = "boolean default false") - private Boolean nominationFacility = false; - - @Column(name = "auto_debit_facility", nullable = false, columnDefinition = "boolean default false") - private Boolean autoDebitFacility = false; - - - @jakarta.persistence.OneToOne(cascade = jakarta.persistence.CascadeType.ALL, mappedBy = "savingsAccount", orphanRemoval = true, fetch = FetchType.LAZY) - private SavingsAccountFacilityConfig facilityConfig; - - @OneToMany(cascade = CascadeType.ALL, mappedBy = "savingsAccount", orphanRemoval = true, fetch = FetchType.LAZY) - private List jointHolders = new ArrayList<>(); - - public String getCbsAccountType() { return cbsAccountType; } - public void setCbsAccountType(String v) { this.cbsAccountType = v; } - - public String getCustomerSegment() { return customerSegment; } - public void setCustomerSegment(String v) { this.customerSegment = v; } - - public String getAccountStatus() { return accountStatus; } - public void setAccountStatus(String v) { this.accountStatus = v; } - - public String getModeOfOperation() { return modeOfOperation; } - public void setModeOfOperation(String v) { this.modeOfOperation = v; } - - public String getSpecialCategory() { return specialCategory; } - public void setSpecialCategory(String v) { this.specialCategory = v; } - - public String getWeakerSection() { return weakerSection; } - public void setWeakerSection(String v) { this.weakerSection = v; } - - public String getGuardianName() { return guardianName; } - public void setGuardianName(String v) { this.guardianName = v; } - - public String getGuardianCifNumber() { return guardianCifNumber; } - public void setGuardianCifNumber(String v) { this.guardianCifNumber = v; } - - public String getGuardianRelationship() { return guardianRelationship; } - public void setGuardianRelationship(String v) { this.guardianRelationship = v; } - - public LocalDate getMinorDateOfBirth() { return minorDateOfBirth; } - public void setMinorDateOfBirth(LocalDate v) { this.minorDateOfBirth = v; } - - public Long getGuardianClientId() { return guardianClientId; } - public void setGuardianClientId(Long v) { this.guardianClientId = v; } - - public Long getIntroducerClientId() { return introducerClientId; } - public void setIntroducerClientId(Long v) { this.introducerClientId = v; } - - public String getIntroducerName() { return introducerName; } - public void setIntroducerName(String v) { this.introducerName = v; } - - public String getIntroducerCifNumber() { return introducerCifNumber; } - public void setIntroducerCifNumber(String v) { this.introducerCifNumber = v; } - - public String getNomineeData() { return nomineeData; } - public void setNomineeData(String v) { this.nomineeData = v; } - - public Long getLinkedAccountId() { return linkedAccountId; } - public void setLinkedAccountId(Long v) { this.linkedAccountId = v; } - - public String getLinkedAccountType() { return linkedAccountType; } - public void setLinkedAccountType(String v) { this.linkedAccountType = v; } - - public String getLinkedAccountNo() { return linkedAccountNo; } - public void setLinkedAccountNo(String v) { this.linkedAccountNo = v; } - - public SavingsAccountFacilityConfig getFacilityConfig() { return facilityConfig; } - public void setFacilityConfig(SavingsAccountFacilityConfig v) { this.facilityConfig = v; } - - public List getJointHolders() { return jointHolders; } - public void setJointHolders(List v) { this.jointHolders = v; } - - public Boolean getNominationFacility() { return nominationFacility; } - public void setNominationFacility(Boolean v) { this.nominationFacility = v; } - - public Boolean getAutoDebitFacility() { return autoDebitFacility; } - public void setAutoDebitFacility(Boolean v) { this.autoDebitFacility = v; } -} +/** + * 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 java.time.temporal.ChronoUnit.DAYS; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.allowOverdraftParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.dueAsOfDateParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.enforceMinRequiredBalanceParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.lienAllowedParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.localeParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.lockinPeriodFrequencyParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.lockinPeriodFrequencyTypeParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.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.nominalAnnualInterestRateOverdraftParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.overdraftLimitParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.withHoldTaxParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.withdrawalFeeForTransfersParamName; + +import com.google.gson.JsonArray; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.DiscriminatorColumn; +import jakarta.persistence.DiscriminatorType; +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OrderBy; +import jakarta.persistence.Table; +import jakarta.persistence.Transient; +import jakarta.persistence.UniqueConstraint; +import jakarta.persistence.Version; +import java.math.BigDecimal; +import java.math.MathContext; +import java.time.LocalDate; +import java.time.YearMonth; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Arrays; +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 java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; +import org.apache.fineract.infrastructure.configuration.service.TemporaryConfigurationServiceContainer; +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.AbstractAuditableWithUTCDateTimeCustom; +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.infrastructure.core.service.ExternalIdFactory; +import org.apache.fineract.infrastructure.core.service.MathUtil; +import org.apache.fineract.infrastructure.security.service.RandomPasswordGenerator; +import org.apache.fineract.interoperation.domain.InteropIdentifier; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.organisation.monetary.domain.MoneyHelper; +import org.apache.fineract.organisation.office.domain.Office; +import org.apache.fineract.organisation.staff.domain.Staff; +import org.apache.fineract.portfolio.accountdetails.domain.AccountType; +import org.apache.fineract.portfolio.charge.domain.Charge; +import org.apache.fineract.portfolio.charge.exception.SavingsAccountChargeNotFoundException; +import org.apache.fineract.portfolio.client.domain.Client; +import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; +import org.apache.fineract.portfolio.group.domain.Group; +import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail; +import org.apache.fineract.portfolio.savings.DepositAccountType; +import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType; +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.exception.InsufficientAccountBalanceException; +import org.apache.fineract.portfolio.savings.exception.SavingsAccountBlockedException; +import org.apache.fineract.portfolio.savings.exception.SavingsAccountCreditsBlockedException; +import org.apache.fineract.portfolio.savings.exception.SavingsAccountDebitsBlockedException; +import org.apache.fineract.portfolio.savings.exception.SavingsAccountTransactionNotFoundException; +import org.apache.fineract.portfolio.savings.exception.SavingsActivityPriorToClientTransferException; +import org.apache.fineract.portfolio.savings.exception.SavingsOfficerAssignmentDateException; +import org.apache.fineract.portfolio.savings.exception.SavingsOfficerUnassignmentDateException; +import org.apache.fineract.portfolio.savings.exception.SavingsTransferTransactionsCannotBeUndoneException; +import org.apache.fineract.portfolio.savings.service.SavingsEnumerations; +import org.apache.fineract.portfolio.tax.domain.TaxComponent; +import org.apache.fineract.portfolio.tax.domain.TaxGroup; +import org.apache.fineract.portfolio.tax.service.TaxUtils; +import org.apache.fineract.useradministration.domain.AppUser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.CollectionUtils; + +@Entity +@Table(name = "m_savings_account", uniqueConstraints = { @UniqueConstraint(columnNames = { "account_no" }, name = "sa_account_no_UNIQUE"), + @UniqueConstraint(columnNames = { "external_id" }, name = "sa_external_id_UNIQUE") }) +@Inheritance(strategy = InheritanceType.SINGLE_TABLE) +@DiscriminatorColumn(name = "deposit_type_enum", discriminatorType = DiscriminatorType.INTEGER) +@DiscriminatorValue("100") +@SuppressWarnings({ "MemberName" }) +public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom implements IDepositAccountType { + + private static final Logger LOG = LoggerFactory.getLogger(SavingsAccount.class); + + @Version + int version; + + @Column(name = "account_no", length = 20, unique = true, nullable = false) + protected String accountNumber; + + @Column(name = "external_id", nullable = true) + protected ExternalId externalId; + + @ManyToOne(optional = true) + @JoinColumn(name = "client_id", nullable = true) + protected Client client; + + @ManyToOne(optional = true, fetch = FetchType.LAZY) + @JoinColumn(name = "group_id", nullable = true) + protected Group group; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "gsim_id", nullable = true) + private GroupSavingsIndividualMonitoring gsim; + + @ManyToOne + @JoinColumn(name = "product_id", nullable = false) + protected SavingsProduct product; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "field_officer_id", nullable = true) + protected Staff savingsOfficer; + + @Column(name = "status_enum", nullable = false) + protected Integer status; + + @Column(name = "sub_status_enum", nullable = false) + protected Integer sub_status = 0; + + @Column(name = "account_type_enum", nullable = false) + protected Integer accountType; + + @Column(name = "submittedon_date", nullable = true) + protected LocalDate submittedOnDate; + + @ManyToOne(optional = true, fetch = FetchType.LAZY) + @JoinColumn(name = "submittedon_userid", nullable = true) + protected AppUser submittedBy; + + @Column(name = "rejectedon_date") + protected LocalDate rejectedOnDate; + + @ManyToOne(optional = true, fetch = FetchType.LAZY) + @JoinColumn(name = "rejectedon_userid", nullable = true) + protected AppUser rejectedBy; + + @Column(name = "withdrawnon_date") + protected LocalDate withdrawnOnDate; + + @ManyToOne(optional = true, fetch = FetchType.LAZY) + @JoinColumn(name = "withdrawnon_userid", nullable = true) + protected AppUser withdrawnBy; + + @Column(name = "approvedon_date") + protected LocalDate approvedOnDate; + + @ManyToOne(optional = true, fetch = FetchType.LAZY) + @JoinColumn(name = "approvedon_userid", nullable = true) + protected AppUser approvedBy; + + @Column(name = "activatedon_date", nullable = true) + protected LocalDate activatedOnDate; + + @ManyToOne(optional = true, fetch = FetchType.LAZY) + @JoinColumn(name = "activatedon_userid", nullable = true) + protected AppUser activatedBy; + + @Column(name = "closedon_date") + protected LocalDate closedOnDate; + + @ManyToOne(optional = true, fetch = FetchType.LAZY) + @JoinColumn(name = "closedon_userid", nullable = true) + protected AppUser closedBy; + + @Column(name = "reason_for_block", nullable = true) + protected String reasonForBlock; + + @Embedded + protected MonetaryCurrency currency; + + @Column(name = "nominal_annual_interest_rate", scale = 6, precision = 19, nullable = false) + protected BigDecimal nominalAnnualInterestRate; + + /** + * The interest period is the span of time at the end of which savings in a client's account earn interest. + * + * A value from the {@link SavingsCompoundingInterestPeriodType} enumeration. + */ + @Column(name = "interest_compounding_period_enum", nullable = false) + protected Integer interestCompoundingPeriodType; + + /** + * A value from the {@link SavingsPostingInterestPeriodType} enumeration. + */ + @Column(name = "interest_posting_period_enum", nullable = false) + protected Integer interestPostingPeriodType; + + /** + * A value from the {@link SavingsInterestCalculationType} enumeration. + */ + @Column(name = "interest_calculation_type_enum", nullable = false) + protected Integer interestCalculationType; + + /** + * A value from the {@link SavingsInterestCalculationDaysInYearType} enumeration. + */ + @Column(name = "interest_calculation_days_in_year_type_enum", nullable = false) + protected Integer interestCalculationDaysInYearType; + + @Column(name = "min_required_opening_balance", scale = 6, precision = 19, nullable = true) + protected BigDecimal minRequiredOpeningBalance; + + @Column(name = "lockin_period_frequency", nullable = true) + protected Integer lockinPeriodFrequency; + + @Column(name = "lockin_period_frequency_enum", nullable = true) + protected Integer lockinPeriodFrequencyType; + + /** + * When account becomes active this field is derived if lockinPeriodFrequency and + * lockinPeriodFrequencyType details are present. + */ + @Column(name = "lockedin_until_date_derived", nullable = true) + protected LocalDate lockedInUntilDate; + + @Column(name = "withdrawal_fee_for_transfer", nullable = true) + protected boolean withdrawalFeeApplicableForTransfer; + + @Column(name = "allow_overdraft") + private boolean allowOverdraft; + + @Column(name = "overdraft_limit", scale = 6, precision = 19, nullable = true) + private BigDecimal overdraftLimit; + + @Column(name = "nominal_annual_interest_rate_overdraft", scale = 6, precision = 19, nullable = true) + protected BigDecimal nominalAnnualInterestRateOverdraft; + + @Column(name = "min_overdraft_for_interest_calculation", scale = 6, precision = 19, nullable = true) + private BigDecimal minOverdraftForInterestCalculation; + + @Column(name = "enforce_min_required_balance") + private boolean enforceMinRequiredBalance; + + @Column(name = "min_required_balance", scale = 6, precision = 19, nullable = true) + private BigDecimal minRequiredBalance; + + @Column(name = "is_lien_allowed", nullable = false) + private boolean lienAllowed; + + @Column(name = "max_allowed_lien_limit", scale = 6, precision = 19, nullable = true) + private BigDecimal maxAllowedLienLimit; + + @Column(name = "on_hold_funds_derived", scale = 6, precision = 19, nullable = true) + private BigDecimal onHoldFunds; + + @Column(name = "start_interest_calculation_date") + protected LocalDate startInterestCalculationDate; + + @Embedded + protected SavingsAccountSummary summary; + + @OrderBy(value = "dateOf, createdDate, id") + @OneToMany(cascade = CascadeType.ALL, mappedBy = "savingsAccount", orphanRemoval = true, fetch = FetchType.LAZY) + protected List transactions = new ArrayList<>(); + + @OneToMany(cascade = CascadeType.ALL, mappedBy = "savingsAccount", orphanRemoval = true, fetch = FetchType.LAZY) + protected Set charges = new HashSet<>(); + + @OneToMany(cascade = CascadeType.ALL, mappedBy = "savingsAccount", orphanRemoval = true, fetch = FetchType.LAZY) + private Set savingsOfficerHistory = new HashSet<>(); + + @Transient + protected boolean accountNumberRequiresAutoGeneration = false; + @Transient + protected SavingsAccountTransactionSummaryWrapper savingsAccountTransactionSummaryWrapper; + @Transient + protected SavingsHelper savingsHelper; + @Transient + protected List savingsAccountTransactions = new ArrayList<>(); + + @Column(name = "deposit_type_enum", insertable = false, updatable = false) + private Integer depositType; + + @Column(name = "min_balance_for_interest_calculation", scale = 6, precision = 19, nullable = true) + private BigDecimal minBalanceForInterestCalculation; + + @Column(name = "withhold_tax", nullable = false) + protected boolean withHoldTax; + + @ManyToOne + @JoinColumn(name = "tax_group_id") + private TaxGroup taxGroup; + + @Column(name = "accrued_till_date") + private LocalDate accruedTillDate; + + @Column(name = "last_closed_business_date") + private LocalDate lastClosedBusinessDate; + + @Column(name = "total_savings_amount_on_hold", scale = 6, precision = 19, nullable = true) + private BigDecimal savingsOnHoldAmount; + @OneToMany(cascade = CascadeType.ALL, mappedBy = "account", orphanRemoval = true, fetch = FetchType.LAZY) + protected List identifiers = new ArrayList<>(); + + public transient ConfigurationDomainService configurationDomainService; + + protected SavingsAccount() { + // + } + + public static SavingsAccount 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 boolean allowOverdraft, final BigDecimal overdraftLimit, final boolean enforceMinRequiredBalance, + final BigDecimal minRequiredBalance, final BigDecimal maxAllowedLienLimit, final boolean lienAllowed, + final BigDecimal nominalAnnualInterestRateOverdraft, final BigDecimal minOverdraftForInterestCalculation, + final boolean withHoldTax) { + + final SavingsAccountStatusType status = SavingsAccountStatusType.SUBMITTED_AND_PENDING_APPROVAL; + return new SavingsAccount(client, group, product, fieldOfficer, accountNo, externalId, status, accountType, submittedOnDate, + submittedBy, interestRate, interestCompoundingPeriodType, interestPostingPeriodType, interestCalculationType, + interestCalculationDaysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType, + withdrawalFeeApplicableForTransfer, savingsAccountCharges, allowOverdraft, overdraftLimit, enforceMinRequiredBalance, + minRequiredBalance, maxAllowedLienLimit, lienAllowed, nominalAnnualInterestRateOverdraft, + minOverdraftForInterestCalculation, withHoldTax); + } + + protected SavingsAccount(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 boolean allowOverdraft, final BigDecimal overdraftLimit, boolean withHoldTax) { + this(client, group, product, fieldOfficer, accountNo, externalId, status, accountType, submittedOnDate, submittedBy, + nominalAnnualInterestRate, interestCompoundingPeriodType, interestPostingPeriodType, interestCalculationType, + interestCalculationDaysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType, + withdrawalFeeApplicableForTransfer, savingsAccountCharges, allowOverdraft, overdraftLimit, false, null, null, false, null, + null, withHoldTax); + } + + protected SavingsAccount(final Client client, final Group group, final SavingsProduct product, final Staff savingsOfficer, + 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 boolean allowOverdraft, final BigDecimal overdraftLimit, final boolean enforceMinRequiredBalance, + final BigDecimal minRequiredBalance, final BigDecimal maxAllowedLienLimit, final boolean lienAllowed, + final BigDecimal nominalAnnualInterestRateOverdraft, final BigDecimal minOverdraftForInterestCalculation, boolean withHoldTax) { + this.client = client; + this.group = group; + this.product = product; + this.savingsOfficer = savingsOfficer; + if (StringUtils.isBlank(accountNo)) { + this.accountNumber = new RandomPasswordGenerator(19).generate(); + this.accountNumberRequiresAutoGeneration = true; + } else { + this.accountNumber = accountNo; + } + + this.currency = product.currency(); + this.externalId = externalId; + this.status = status.getValue(); + this.accountType = accountType.getValue(); + this.submittedOnDate = submittedOnDate; + this.submittedBy = submittedBy; + this.nominalAnnualInterestRate = nominalAnnualInterestRate; + this.interestCompoundingPeriodType = interestCompoundingPeriodType.getValue(); + this.interestPostingPeriodType = interestPostingPeriodType.getValue(); + this.interestCalculationType = interestCalculationType.getValue(); + this.interestCalculationDaysInYearType = interestCalculationDaysInYearType.getValue(); + this.minRequiredOpeningBalance = minRequiredOpeningBalance; + this.lockinPeriodFrequency = lockinPeriodFrequency; + if (lockinPeriodFrequencyType != null) { + this.lockinPeriodFrequencyType = lockinPeriodFrequencyType.getValue(); + } + this.withdrawalFeeApplicableForTransfer = withdrawalFeeApplicableForTransfer; + + if (!CollectionUtils.isEmpty(savingsAccountCharges)) { + this.charges = associateChargesWithThisSavingsAccount(savingsAccountCharges); + } + + this.summary = new SavingsAccountSummary(); + this.allowOverdraft = allowOverdraft; + this.overdraftLimit = overdraftLimit; + this.nominalAnnualInterestRateOverdraft = nominalAnnualInterestRateOverdraft; + this.minOverdraftForInterestCalculation = minOverdraftForInterestCalculation; + esnureOverdraftLimitsSetForOverdraftAccounts(); + + this.enforceMinRequiredBalance = enforceMinRequiredBalance; + this.minRequiredBalance = minRequiredBalance; + this.lienAllowed = lienAllowed; + this.maxAllowedLienLimit = maxAllowedLienLimit; + this.minBalanceForInterestCalculation = product.minBalanceForInterestCalculation(); + // this.savingsOfficerHistory = null; + this.withHoldTax = withHoldTax; + this.taxGroup = product.getTaxGroup(); + } + + /** + * Used after fetching/hydrating a {@link SavingsAccount} object to inject helper services/components used for + * update summary details after events/transactions on a {@link SavingsAccount}. + */ + public void setHelpers(final SavingsAccountTransactionSummaryWrapper savingsAccountTransactionSummaryWrapper, + final SavingsHelper savingsHelper) { + this.savingsAccountTransactionSummaryWrapper = savingsAccountTransactionSummaryWrapper; + this.savingsHelper = savingsHelper; + } + + public void setSavingsAccountTransactions(final List savingsAccountTransactions) { + this.savingsAccountTransactions.addAll(savingsAccountTransactions); + } + + public List getSavingsAccountTransactionsWithPivotConfig() { + return this.savingsAccountTransactions; + } + + public ExternalId getExternalId() { + return externalId; + } + + public boolean isNotActive() { + return !isActive(); + } + + public boolean isActive() { + return SavingsAccountStatusType.fromInt(this.status).isActive(); + } + + public boolean isNotSubmittedAndPendingApproval() { + return !isSubmittedAndPendingApproval(); + } + + public boolean isSubmittedAndPendingApproval() { + return SavingsAccountStatusType.fromInt(this.status).isSubmittedAndPendingApproval(); + } + + public boolean isApproved() { + return SavingsAccountStatusType.fromInt(this.status).isApproved(); + } + + public boolean isActivated() { + boolean isActive = false; + if (this.activatedOnDate != null) { + isActive = true; + } + return isActive; + } + + public boolean isClosed() { + return SavingsAccountStatusType.fromInt(this.status).isClosed(); + } + + public List getIdentifiers() { + return identifiers; + } + + public void postInterest(final MathContext mc, final LocalDate interestPostingUpToDate, final boolean isInterestTransfer, + final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth, + final LocalDate postInterestOnDate, final boolean backdatedTxnsAllowedTill, final boolean postReversals) { + final List postingPeriods = calculateInterestUsing(mc, interestPostingUpToDate, isInterestTransfer, + isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, + postReversals); + if (postingPeriods.isEmpty()) { + return; + } + + Money interestPostedToDate = Money.zero(this.currency); + + if (backdatedTxnsAllowedTill) { + interestPostedToDate = Money.of(this.currency, this.summary.getTotalInterestPosted()); + } + + boolean recalucateDailyBalanceDetails = false; + boolean applyWithHoldTax = isWithHoldTaxApplicableForInterestPosting(); + final List withholdTransactions = new ArrayList<>(); + + if (backdatedTxnsAllowedTill) { + withholdTransactions.addAll(findWithHoldSavingsTransactionsWithPivotConfig()); + } else { + withholdTransactions.addAll(findWithHoldTransactions()); + } + + for (final PostingPeriod interestPostingPeriod : postingPeriods) { + final LocalDate interestPostingTransactionDate = interestPostingPeriod.dateOfPostingTransaction(); + final Money interestEarnedToBePostedForPeriod = interestPostingPeriod.getInterestEarned(); + + if (!DateUtils.isAfter(interestPostingTransactionDate, interestPostingUpToDate)) { + interestPostedToDate = interestPostedToDate.plus(interestEarnedToBePostedForPeriod); + + SavingsAccountTransaction postingTransaction = null; + if (backdatedTxnsAllowedTill) { + postingTransaction = findInterestPostingSavingsTransactionWithPivotConfig(interestPostingTransactionDate); + } else { + postingTransaction = findInterestPostingTransactionFor(interestPostingTransactionDate); + } + if (postingTransaction == null) { + SavingsAccountTransaction newPostingTransaction; + if (interestEarnedToBePostedForPeriod.isGreaterThanOrEqualTo(Money.zero(currency))) { + + newPostingTransaction = SavingsAccountTransaction.interestPosting(this, office(), interestPostingTransactionDate, + interestEarnedToBePostedForPeriod, interestPostingPeriod.isUserPosting()); + } else { + newPostingTransaction = SavingsAccountTransaction.overdraftInterest(this, office(), interestPostingTransactionDate, + interestEarnedToBePostedForPeriod.negated(), interestPostingPeriod.isUserPosting()); + } + if (backdatedTxnsAllowedTill) { + addTransactionToExisting(newPostingTransaction); + } else { + addTransaction(newPostingTransaction); + } + if (applyWithHoldTax) { + createWithHoldTransaction(interestEarnedToBePostedForPeriod.getAmount(), interestPostingTransactionDate, + backdatedTxnsAllowedTill); + } + recalucateDailyBalanceDetails = true; + } else { + boolean correctionRequired = false; + if (postingTransaction.isInterestPostingAndNotReversed()) { + correctionRequired = postingTransaction.hasNotAmount(interestEarnedToBePostedForPeriod); + } else { + correctionRequired = postingTransaction.hasNotAmount(interestEarnedToBePostedForPeriod.negated()); + } + if (correctionRequired) { + boolean applyWithHoldTaxForOldTransaction = false; + postingTransaction.reverse(); + SavingsAccountTransaction reversal = null; + if (postReversals) { + reversal = SavingsAccountTransaction.reversal(postingTransaction); + } + final SavingsAccountTransaction withholdTransaction = findTransactionFor(interestPostingTransactionDate, + withholdTransactions); + if (withholdTransaction != null) { + withholdTransaction.reverse(); + applyWithHoldTaxForOldTransaction = true; + } + SavingsAccountTransaction newPostingTransaction; + if (interestEarnedToBePostedForPeriod.isGreaterThanOrEqualTo(Money.zero(currency))) { + newPostingTransaction = SavingsAccountTransaction.interestPosting(this, office(), + interestPostingTransactionDate, interestEarnedToBePostedForPeriod, + interestPostingPeriod.isUserPosting()); + } else { + newPostingTransaction = SavingsAccountTransaction.overdraftInterest(this, office(), + interestPostingTransactionDate, interestEarnedToBePostedForPeriod.negated(), + interestPostingPeriod.isUserPosting()); + } + if (backdatedTxnsAllowedTill) { + addTransactionToExisting(newPostingTransaction); + if (reversal != null) { + addTransactionToExisting(reversal); + } + } else { + addTransaction(newPostingTransaction); + if (reversal != null) { + addTransaction(reversal); + } + } + if (applyWithHoldTaxForOldTransaction) { + createWithHoldTransaction(interestEarnedToBePostedForPeriod.getAmount(), interestPostingTransactionDate, + backdatedTxnsAllowedTill); + } + recalucateDailyBalanceDetails = true; + } + } + } + } + + if (recalucateDailyBalanceDetails) { + // no openingBalance concept supported yet but probably will to + // allow + // for migrations. + Money openingAccountBalance = Money.zero(this.currency); + + if (backdatedTxnsAllowedTill) { + if (this.summary.getLastInterestCalculationDate() == null) { + openingAccountBalance = Money.zero(this.currency); + } else { + openingAccountBalance = Money.of(this.currency, this.summary.getRunningBalanceOnPivotDate()); + } + } + + // update existing transactions so derived balance fields are + // correct. + recalculateDailyBalances(openingAccountBalance, interestPostingUpToDate, backdatedTxnsAllowedTill, postReversals); + } + + if (!backdatedTxnsAllowedTill) { + this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, this.transactions); + } else { + this.summary.updateSummaryWithPivotConfig(this.currency, this.savingsAccountTransactionSummaryWrapper, null, + this.savingsAccountTransactions); + } + } + + protected List findWithHoldTransactions() { + final List withholdTransactions = new ArrayList<>(); + List trans = getTransactions(); + for (final SavingsAccountTransaction transaction : trans) { + if (transaction.isWithHoldTaxAndNotReversed()) { + withholdTransactions.add(transaction); + } + } + return withholdTransactions; + } + + protected List findWithHoldSavingsTransactionsWithPivotConfig() { + final List withholdTransactions = new ArrayList<>(); + List trans = getSavingsAccountTransactionsWithPivotConfig(); + for (final SavingsAccountTransaction transaction : trans) { + if (transaction.isWithHoldTaxAndNotReversed() && !transaction.isReversalTransaction()) { + withholdTransactions.add(transaction); + } + } + return withholdTransactions; + } + + private boolean isWithHoldTaxApplicableForInterestPosting() { + return this.withHoldTax() && this.depositAccountType().isSavingsDeposit(); + } + + protected SavingsAccountTransaction findInterestPostingTransactionFor(final LocalDate postingDate) { + SavingsAccountTransaction postingTransation = null; + List trans = getTransactions(); + for (final SavingsAccountTransaction transaction : trans) { + if ((transaction.isInterestPostingAndNotReversed() || transaction.isOverdraftInterestAndNotReversed()) + && transaction.occursOn(postingDate) && !transaction.isReversalTransaction()) { + postingTransation = transaction; + break; + } + } + return postingTransation; + } + + protected SavingsAccountTransaction findInterestPostingSavingsTransactionWithPivotConfig(final LocalDate postingDate) { + SavingsAccountTransaction postingTransation = null; + List trans = getSavingsAccountTransactionsWithPivotConfig(); + for (final SavingsAccountTransaction transaction : trans) { + if ((transaction.isInterestPostingAndNotReversed() || transaction.isOverdraftInterestAndNotReversed()) + && transaction.occursOn(postingDate) && !transaction.isReversalTransaction()) { + postingTransation = transaction; + break; + } + } + return postingTransation; + } + + protected SavingsAccountTransaction findTransactionFor(final LocalDate postingDate, + final List transactions) { + SavingsAccountTransaction transaction = null; + for (final SavingsAccountTransaction savingsAccountTransaction : transactions) { + if (savingsAccountTransaction.occursOn(postingDate)) { + transaction = savingsAccountTransaction; + break; + } + } + return transaction; + } + + protected boolean createWithHoldTransaction(final BigDecimal amount, final LocalDate date, final boolean backdatedTxnsAllowedTill) { + boolean isTaxAdded = false; + if (this.taxGroup != null && amount.compareTo(BigDecimal.ZERO) > 0) { + Map taxSplit = TaxUtils.splitTax(amount, date, this.taxGroup.getTaxGroupMappings(), amount.scale()); + BigDecimal totalTax = TaxUtils.totalTaxAmount(taxSplit); + if (totalTax.compareTo(BigDecimal.ZERO) > 0) { + SavingsAccountTransaction withholdTransaction = SavingsAccountTransaction.withHoldTax(this, office(), date, + Money.of(currency, totalTax), taxSplit); + if (backdatedTxnsAllowedTill) { + addTransactionToExisting(withholdTransaction); + } else { + addTransaction(withholdTransaction); + } + isTaxAdded = true; + } + } + return isTaxAdded; + } + + protected boolean updateWithHoldTransaction(final BigDecimal amount, final SavingsAccountTransaction withholdTransaction) { + boolean isTaxAdded = false; + if (this.taxGroup != null && amount.compareTo(BigDecimal.ZERO) > 0) { + Map taxSplit = TaxUtils.splitTax(amount, withholdTransaction.getTransactionDate(), + this.taxGroup.getTaxGroupMappings(), amount.scale()); + BigDecimal totalTax = TaxUtils.totalTaxAmount(taxSplit); + if (totalTax.compareTo(BigDecimal.ZERO) > 0) { + if (withholdTransaction.getId() == null) { + withholdTransaction.setAmount(Money.of(currency, totalTax)); + withholdTransaction.getTaxDetails().clear(); + SavingsAccountTransaction.updateTaxDetails(taxSplit, withholdTransaction); + isTaxAdded = true; + } else if (totalTax.compareTo(withholdTransaction.getAmount()) != 0) { + withholdTransaction.reverse(); + SavingsAccountTransaction newWithholdTransaction = SavingsAccountTransaction.withHoldTax(this, office(), + withholdTransaction.getTransactionDate(), Money.of(currency, totalTax), taxSplit); + addTransaction(newWithholdTransaction); + isTaxAdded = true; + } + } + } + return isTaxAdded; + } + + // Determine the last transaction for given day + protected SavingsAccountTransaction findLastTransaction(final LocalDate date) { + + SavingsAccountTransaction savingsTransaction = null; + List trans = getTransactions(); + for (final SavingsAccountTransaction transaction : trans) { + if (transaction.isNotReversed() && !transaction.isReversalTransaction() && transaction.occursOn(date)) { + savingsTransaction = transaction; + break; + } + } + + return savingsTransaction; + } + + protected SavingsAccountTransaction findLastFilteredTransactionWithPivotConfig(final LocalDate date) { + SavingsAccountTransaction savingsTransaction = null; + List trans = getSavingsAccountTransactionsWithPivotConfig(); + for (final SavingsAccountTransaction transaction : trans) { + if (transaction.isNotReversed() && !transaction.isReversalTransaction() && transaction.occursOn(date)) { + savingsTransaction = transaction; + break; + } + } + return savingsTransaction; + } + + public List getManualPostingDates() { + List transactions = new ArrayList<>(); + for (SavingsAccountTransaction trans : this.transactions) { + if (trans.isInterestPosting() && trans.isNotReversed() && !trans.isReversalTransaction() && trans.isManualTransaction()) { + transactions.add(trans.getTransactionDate()); + } + } + return transactions; + } + + public List getManualPostingDatesWithPivotConfig() { + List transactions = new ArrayList<>(); + for (SavingsAccountTransaction trans : this.savingsAccountTransactions) { + if (trans.isInterestPosting() && trans.isNotReversed() && trans.isManualTransaction()) { + transactions.add(trans.getTransactionDate()); + } + } + return transactions; + } + + /** + * All interest calculation based on END-OF-DAY-BALANCE. + * + * Interest calculation is performed on-the-fly over all account transactions. + * + * + * 1. Calculate Interest From Beginning Of Account 1a. determine the 'crediting' periods that exist for this savings + * acccount 1b. determine the 'compounding' periods that exist within each 'crediting' period calculate the amount + * of interest due at the end of each 'crediting' period check if an existing 'interest posting' transaction exists + * for date and matches the amount posted + * + * @param isInterestTransfer + * TODO + */ + + public List calculateInterestUsing(final MathContext mc, final LocalDate upToInterestCalculationDate, + boolean isInterestTransfer, final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth, + final LocalDate postInterestOnDate, final boolean backdatedTxnsAllowedTill, final boolean postReversals) { + + // no openingBalance concept supported yet but probably will to allow for migrations. + // Check global configurations and 'pivot' date is null + Money openingAccountBalance = backdatedTxnsAllowedTill ? Money.of(this.currency, this.summary.getRunningBalanceOnPivotDate()) + : Money.zero(this.currency); + + // update existing transactions so derived balance fields are correct. + recalculateDailyBalances(openingAccountBalance, upToInterestCalculationDate, backdatedTxnsAllowedTill, postReversals); + + final List allPostingPeriods = new ArrayList<>(); + if (hasInterestCalculation() || hasOverdraftInterestCalculation()) { + // 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 = null; + if (backdatedTxnsAllowedTill) { + postedAsOnDates = getManualPostingDatesWithPivotConfig(); + } else { + postedAsOnDates = getManualPostingDates(); + } + if (postInterestOnDate != null) { + postedAsOnDates.add(postInterestOnDate); + } + final List postingPeriodIntervals = this.savingsHelper.determineInterestPostingPeriods( + getStartInterestCalculationDate(), upToInterestCalculationDate, postingPeriodType, financialYearBeginningMonth, + postedAsOnDates); + + Money periodStartingBalance; + if (this.startInterestCalculationDate != null && !this.getStartInterestCalculationDate().equals(this.getActivationDate())) { + LocalDate startInterestCalculationDate = this.startInterestCalculationDate; + SavingsAccountTransaction transaction = null; + if (backdatedTxnsAllowedTill) { + transaction = findLastFilteredTransactionWithPivotConfig(startInterestCalculationDate); + } else { + transaction = findLastTransaction(startInterestCalculationDate); + } + + if (transaction == null) { + periodStartingBalance = Money.zero(this.currency); + } else { + periodStartingBalance = Money.of(this.currency, this.summary.getRunningBalanceOnPivotDate()); + } + } else { + periodStartingBalance = Money.zero(this.currency); + } + + final SavingsInterestCalculationType interestCalculationType = SavingsInterestCalculationType + .fromInt(this.interestCalculationType); + final BigDecimal interestRateAsFraction = getEffectiveInterestRateAsFraction(mc, upToInterestCalculationDate); + final BigDecimal overdraftInterestRateAsFraction = getEffectiveOverdraftInterestRateAsFraction(mc); + final Collection interestPostTransactions = this.savingsHelper.fetchPostInterestTransactionIds(getId()); + final Money minBalanceForInterestCalculation = Money.of(getCurrency(), minBalanceForInterestCalculation()); + final Money minOverdraftForInterestCalculation = Money.of(getCurrency(), this.minOverdraftForInterestCalculation); + + for (final LocalDateInterval periodInterval : postingPeriodIntervals) { + + boolean isUserPosting = false; + if (postedAsOnDates.contains(periodInterval.endDate().plusDays(1))) { + isUserPosting = true; + } + + PostingPeriod postingPeriod = null; + List orderedNonInterestPostingTransactions = null; + if (backdatedTxnsAllowedTill) { + orderedNonInterestPostingTransactions = retreiveOrderedNonInterestPostingSavingsTransactionsWithPivotConfig(); + } else { + orderedNonInterestPostingTransactions = retreiveOrderedNonInterestPostingTransactions(); + } + + List savingsAccountTransactionDetailsForPostingPeriod = toSavingsAccountTransactionDetailsForPostingPeriodList( + orderedNonInterestPostingTransactions); + + postingPeriod = PostingPeriod.createFrom(periodInterval, periodStartingBalance, + savingsAccountTransactionDetailsForPostingPeriod, this.currency, compoundingPeriodType, interestCalculationType, + interestRateAsFraction, daysInYearType.getValue(), upToInterestCalculationDate, interestPostTransactions, + isInterestTransfer, minBalanceForInterestCalculation, isSavingsInterestPostingAtCurrentPeriodEnd, + overdraftInterestRateAsFraction, minOverdraftForInterestCalculation, isUserPosting, financialYearBeginningMonth); + + periodStartingBalance = postingPeriod.closingBalance(); + + allPostingPeriods.add(postingPeriod); + } + + this.savingsHelper.calculateInterestForAllPostingPeriods(this.currency, allPostingPeriods, getLockedInUntilDate(), + isTransferInterestToOtherAccount()); + + this.summary.updateFromInterestPeriodSummaries(this.currency, allPostingPeriods); + } + + if (backdatedTxnsAllowedTill) { + this.summary.updateSummaryWithPivotConfig(this.currency, this.savingsAccountTransactionSummaryWrapper, null, + this.savingsAccountTransactions); + } else { + this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, this.transactions); + } + + return allPostingPeriods; + } + + private BigDecimal getEffectiveOverdraftInterestRateAsFraction(MathContext mc) { + return this.nominalAnnualInterestRateOverdraft.divide(BigDecimal.valueOf(100L), mc); + } + + public BigDecimal getEffectiveInterestRateAsFractionAccrual(final MathContext mc, final LocalDate upToInterestCalculationDate) { + return this.nominalAnnualInterestRate.divide(BigDecimal.valueOf(100L), mc); + } + + @SuppressWarnings("unused") + protected BigDecimal getEffectiveInterestRateAsFraction(final MathContext mc, final LocalDate upToInterestCalculationDate) { + return this.nominalAnnualInterestRate.divide(BigDecimal.valueOf(100L), mc); + } + + private boolean hasInterestCalculation() { + return !MathUtil.isEmpty(nominalAnnualInterestRate); + } + + private boolean hasOverdraftInterestCalculation() { + return isAllowOverdraft() && !MathUtil.isEmpty(getOverdraftLimit()) && !MathUtil.isEmpty(nominalAnnualInterestRateOverdraft); + } + + public List retrieveOrderedAccrualTransactions() { + return retrieveListOfTransactions().stream().filter(SavingsAccountTransaction::isAccrual) + .sorted(new SavingsAccountTransactionComparator()).collect(Collectors.toList()); + } + + protected List retreiveOrderedNonInterestPostingTransactions() { + final List listOfTransactionsSorted = retrieveListOfTransactions(); + + final List orderedNonInterestPostingTransactions = new ArrayList<>(); + + for (final SavingsAccountTransaction transaction : listOfTransactionsSorted) { + if (!(transaction.isInterestPostingAndNotReversed() || transaction.isOverdraftInterestAndNotReversed()) + && transaction.isNotReversed() && !transaction.isReversalTransaction()) { + orderedNonInterestPostingTransactions.add(transaction); + } + } + orderedNonInterestPostingTransactions.sort(new SavingsAccountTransactionComparator()); + return orderedNonInterestPostingTransactions; + } + + protected List retreiveOrderedNonInterestPostingSavingsTransactionsWithPivotConfig() { + final List listOfTransactionsSorted = retrieveSortedTransactions(); + + final List orderedNonInterestPostingTransactions = new ArrayList<>(); + + for (final SavingsAccountTransaction transaction : listOfTransactionsSorted) { + if (!(transaction.isInterestPostingAndNotReversed() || transaction.isOverdraftInterestAndNotReversed()) + && transaction.isNotReversed() && !transaction.isReversalTransaction()) { + orderedNonInterestPostingTransactions.add(transaction); + } + } + orderedNonInterestPostingTransactions.sort(new SavingsAccountTransactionComparator()); + return orderedNonInterestPostingTransactions; + } + + protected List retrieveSortedTransactions() { + final List listOfTransactionsSorted = new ArrayList<>(); + listOfTransactionsSorted.addAll(this.savingsAccountTransactions); + final SavingsAccountTransactionComparator transactionComparator = new SavingsAccountTransactionComparator(); + Collections.sort(listOfTransactionsSorted, transactionComparator); + return listOfTransactionsSorted; + } + + protected List retrieveListOfTransactions() { + final List listOfTransactionsSorted = new ArrayList<>(); + listOfTransactionsSorted.addAll(this.transactions); + + final SavingsAccountTransactionComparator transactionComparator = new SavingsAccountTransactionComparator(); + Collections.sort(listOfTransactionsSorted, transactionComparator); + return listOfTransactionsSorted; + } + + protected void recalculateDailyBalances(final Money openingAccountBalance, final LocalDate interestPostingUpToDate, + final boolean backdatedTxnsAllowedTill, boolean postReversals) { + Money runningBalance = openingAccountBalance; + boolean calculateInterest = hasInterestCalculation() || hasOverdraftInterestCalculation(); + + List accountTransactionsSorted = null; + + if (backdatedTxnsAllowedTill) { + accountTransactionsSorted = retrieveSortedTransactions(); + } else { + accountTransactionsSorted = retrieveListOfTransactions(); + } + + boolean isTransactionsModified = false; + for (final SavingsAccountTransaction transaction : accountTransactionsSorted) { + if (transaction.isReversed() || transaction.isReversalTransaction()) { + transaction.zeroBalanceFields(); + } else { + Money overdraftAmount = Money.zero(this.currency); + Money transactionAmount = Money.zero(this.currency); + if ((transaction.isCredit() || transaction.isAmountRelease())) { + if (runningBalance.isLessThanZero()) { + Money diffAmount = transaction.getAmount(this.currency).plus(runningBalance); + if (diffAmount.isGreaterThanZero()) { + overdraftAmount = transaction.getAmount(this.currency).minus(diffAmount); + } else { + overdraftAmount = transaction.getAmount(this.currency); + } + } + transactionAmount = transactionAmount.plus(transaction.getAmount(this.currency)); + } else if (transaction.isDebit() || transaction.isAmountOnHold()) { + if (runningBalance.isLessThanZero()) { + overdraftAmount = transaction.getAmount(this.currency); + } + transactionAmount = transactionAmount.minus(transaction.getAmount(this.currency)); + } + + runningBalance = runningBalance.plus(transactionAmount); + transaction.setRunningBalance(runningBalance); + + if (MathUtil.isEmpty(overdraftAmount) && runningBalance.isLessThanZero() && !transaction.isAmountOnHold()) { + overdraftAmount = runningBalance.negated(); + } + if (!calculateInterest || transaction.getId() == null || transaction.getOverdraftAmount(this.currency).isZero()) { + transaction.setOverdraftAmount(overdraftAmount); + } else if (!MathUtil.isEqualTo(overdraftAmount, transaction.getOverdraftAmount(this.currency)) + && !transaction.isAccrual()) { + SavingsAccountTransaction accountTransaction = SavingsAccountTransaction.copyTransaction(transaction); + if (transaction.isChargeTransaction()) { + Set chargesPaidBy = transaction.getSavingsAccountChargesPaid(); + final Set newChargePaidBy = new HashSet<>(); + chargesPaidBy.forEach(x -> newChargePaidBy + .add(SavingsAccountChargePaidBy.instance(accountTransaction, x.getSavingsAccountCharge(), x.getAmount()))); + accountTransaction.getSavingsAccountChargesPaid().addAll(newChargePaidBy); + } + SavingsAccountTransaction reversal = null; + transaction.reverse(); + if (postReversals) { + reversal = SavingsAccountTransaction.reversal(transaction); + } + if (MathUtil.isGreaterThanZero(overdraftAmount)) { + accountTransaction.setOverdraftAmount(overdraftAmount); + } + accountTransaction.setRunningBalance(runningBalance); + if (backdatedTxnsAllowedTill) { + addTransactionToExisting(accountTransaction); + if (reversal != null) { + addTransactionToExisting(reversal); + } + } else { + addTransaction(accountTransaction); + if (reversal != null) { + addTransaction(reversal); + } + } + isTransactionsModified = true; + } + + } + } + + if (isTransactionsModified) { + if (backdatedTxnsAllowedTill) { + accountTransactionsSorted = retrieveSortedTransactions(); + } else { + accountTransactionsSorted = retrieveListOfTransactions(); + } + } + resetAccountTransactionsEndOfDayBalances(accountTransactionsSorted, interestPostingUpToDate); + } + + protected void resetAccountTransactionsEndOfDayBalances(final List accountTransactionsSorted, + final LocalDate interestPostingUpToDate) { + // loop over transactions in reverse + LocalDate endOfBalanceDate = interestPostingUpToDate; + for (int i = accountTransactionsSorted.size() - 1; i >= 0; i--) { + final SavingsAccountTransaction transaction = accountTransactionsSorted.get(i); + if (transaction.isNotReversed() && !transaction.isReversalTransaction() + && !(transaction.isInterestPostingAndNotReversed() || transaction.isOverdraftInterestAndNotReversed())) { + transaction.updateCumulativeBalanceAndDates(this.currency, endOfBalanceDate); + // this transactions transaction date is end of balance date for + // previous transaction. + endOfBalanceDate = transaction.getTransactionDate().minusDays(1); + } + } + } + + public SavingsAccountTransaction deposit(final SavingsAccountTransactionDTO transactionDTO, final boolean backdatedTxnsAllowedTill, + final Long relaxingDaysConfigForPivotDate, final String refNo) { + return deposit(transactionDTO, SavingsAccountTransactionType.DEPOSIT, backdatedTxnsAllowedTill, relaxingDaysConfigForPivotDate, + refNo); + } + + public SavingsAccountTransaction dividendPayout(final SavingsAccountTransactionDTO transactionDTO, + final boolean backdatedTxnsAllowedTill, final Long relaxingDaysConfigForPivotDate) { + String refNo = null; + return deposit(transactionDTO, SavingsAccountTransactionType.DIVIDEND_PAYOUT, backdatedTxnsAllowedTill, + relaxingDaysConfigForPivotDate, refNo); + } + + public SavingsAccountTransaction deposit(final SavingsAccountTransactionDTO transactionDTO, + final SavingsAccountTransactionType savingsAccountTransactionType, final boolean backdatedTxnsAllowedTill, + final Long relaxingDaysConfigForPivotDate, final String refNo) { + final String resourceTypeName = depositAccountType().resourceName(); + if (isNotActive()) { + final String defaultUserMessage = "Transaction is not allowed. Account is not active."; + final ApiParameterError error = ApiParameterError.parameterError( + "error.msg." + resourceTypeName + ".transaction.account.is.not.active", defaultUserMessage, "transactionDate", + transactionDTO.getTransactionDate().format(transactionDTO.getFormatter())); + + final List dataValidationErrors = new ArrayList<>(); + dataValidationErrors.add(error); + + throw new PlatformApiDataValidationException(dataValidationErrors); + } + + if (DateUtils.isDateInTheFuture(transactionDTO.getTransactionDate())) { + final String defaultUserMessage = "Transaction date cannot be in the future."; + final ApiParameterError error = ApiParameterError.parameterError("error.msg." + resourceTypeName + ".transaction.in.the.future", + defaultUserMessage, "transactionDate", transactionDTO.getTransactionDate().format(transactionDTO.getFormatter())); + + final List dataValidationErrors = new ArrayList<>(); + dataValidationErrors.add(error); + + throw new PlatformApiDataValidationException(dataValidationErrors); + } + + if (DateUtils.isBefore(transactionDTO.getTransactionDate(), getActivationDate())) { + final Object[] defaultUserArgs = Arrays.asList(transactionDTO.getTransactionDate().format(transactionDTO.getFormatter()), + getActivationDate().format(transactionDTO.getFormatter())).toArray(); + final String defaultUserMessage = "Transaction date cannot be before accounts activation date."; + final ApiParameterError error = ApiParameterError.parameterError( + "error.msg." + resourceTypeName + ".transaction.before.activation.date", defaultUserMessage, "transactionDate", + defaultUserArgs); + + final List dataValidationErrors = new ArrayList<>(); + dataValidationErrors.add(error); + + throw new PlatformApiDataValidationException(dataValidationErrors); + } + validatePivotDateTransaction(transactionDTO.getTransactionDate(), backdatedTxnsAllowedTill, relaxingDaysConfigForPivotDate, + resourceTypeName); + + validateActivityNotBeforeClientOrGroupTransferDate(SavingsEvent.SAVINGS_DEPOSIT, transactionDTO.getTransactionDate()); + + final Money amount = Money.of(this.currency, transactionDTO.getTransactionAmount()); + + final SavingsAccountTransaction transaction = SavingsAccountTransaction.deposit(this, office(), transactionDTO.getPaymentDetail(), + transactionDTO.getTransactionDate(), amount, savingsAccountTransactionType, refNo); + + if (backdatedTxnsAllowedTill) { + addTransactionToExisting(transaction); + } else { + this.accrualsForSavingsReverse(transactionDTO, backdatedTxnsAllowedTill); + addTransaction(transaction); + } + + if (this.sub_status.equals(SavingsAccountSubStatusEnum.INACTIVE.getValue()) + || this.sub_status.equals(SavingsAccountSubStatusEnum.DORMANT.getValue())) { + this.sub_status = SavingsAccountSubStatusEnum.NONE.getValue(); + } + + if (backdatedTxnsAllowedTill) { + this.summary.updateSummaryWithPivotConfig(this.currency, this.savingsAccountTransactionSummaryWrapper, transaction, + this.savingsAccountTransactions); + } + + return transaction; + } + + public void validatePivotDateTransaction(LocalDate transactionDate, final boolean backdatedTxnsAllowedTill, + final Long relaxingDaysConfigForPivotDate, final String resourceTypeName) { + if (backdatedTxnsAllowedTill) { + if (this.getSummary().getInterestPostedTillDate() != null && DateUtils.isBefore(transactionDate, + getSummary().getInterestPostedTillDate().minusDays(relaxingDaysConfigForPivotDate))) { + final Object[] defaultUserArgs = Arrays.asList(transactionDate, getActivationDate()).toArray(); + final String defaultUserMessage = "Transaction date cannot be before transactions pivot date."; + final ApiParameterError error = ApiParameterError.parameterError( + "error.msg." + resourceTypeName + ".transaction.before.pivot.date", defaultUserMessage, "transactionDate", + defaultUserArgs); + + final List dataValidationErrors = new ArrayList<>(); + dataValidationErrors.add(error); + + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + } + + public LocalDate getActivationDate() { + return this.activatedOnDate; + } + + public AppUser getActivatedBy() { + return this.activatedBy; + } + + public LocalDate getWithdrawnOnDate() { + return this.withdrawnOnDate; + } + + public AppUser getWithdrawnBy() { + return this.withdrawnBy; + } + + // startInterestCalculationDate is set during migration so that there is no + // interference with interest posting of previous system + public LocalDate getStartInterestCalculationDate() { + LocalDate startInterestCalculationLocalDate = null; + if (this.startInterestCalculationDate != null) { + startInterestCalculationLocalDate = this.startInterestCalculationDate; + } else { + startInterestCalculationLocalDate = getActivationDate(); + } + return startInterestCalculationLocalDate; + } + + public void setStartInterestCalculationDate(LocalDate startInterestCalculationDate) { + this.startInterestCalculationDate = startInterestCalculationDate; + } + + public SavingsAccountTransaction withdraw(final SavingsAccountTransactionDTO transactionDTO, final boolean applyWithdrawFee, + final boolean backdatedTxnsAllowedTill, final Long relaxingDaysConfigForPivotDate, String refNo) { + if (!isTransactionsAllowed()) { + + final String defaultUserMessage = "Transaction is not allowed. Account is not active."; + final ApiParameterError error = ApiParameterError.parameterError("error.msg.savingsaccount.transaction.account.is.not.active", + defaultUserMessage, "transactionDate", transactionDTO.getTransactionDate().format(transactionDTO.getFormatter())); + + final List dataValidationErrors = new ArrayList<>(); + dataValidationErrors.add(error); + + throw new PlatformApiDataValidationException(dataValidationErrors); + } + + if (DateUtils.isDateInTheFuture(transactionDTO.getTransactionDate())) { + final String defaultUserMessage = "Transaction date cannot be in the future."; + final ApiParameterError error = ApiParameterError.parameterError("error.msg.savingsaccount.transaction.in.the.future", + defaultUserMessage, "transactionDate", transactionDTO.getTransactionDate().format(transactionDTO.getFormatter())); + + final List dataValidationErrors = new ArrayList<>(); + dataValidationErrors.add(error); + + throw new PlatformApiDataValidationException(dataValidationErrors); + } + + if (DateUtils.isBefore(transactionDTO.getTransactionDate(), getActivationDate())) { + final Object[] defaultUserArgs = Arrays.asList(transactionDTO.getTransactionDate().format(transactionDTO.getFormatter()), + getActivationDate().format(transactionDTO.getFormatter())).toArray(); + final String defaultUserMessage = "Transaction date cannot be before accounts activation date."; + final ApiParameterError error = ApiParameterError.parameterError("error.msg.savingsaccount.transaction.before.activation.date", + defaultUserMessage, "transactionDate", defaultUserArgs); + + final List dataValidationErrors = new ArrayList<>(); + dataValidationErrors.add(error); + + throw new PlatformApiDataValidationException(dataValidationErrors); + } + + if (isAccountLocked(transactionDTO.getTransactionDate())) { + final String defaultUserMessage = "Withdrawal is not allowed. No withdrawals are allowed until after " + + getLockedInUntilDate().format(transactionDTO.getFormatter()); + final ApiParameterError error = ApiParameterError.parameterError( + "error.msg.savingsaccount.transaction.withdrawals.blocked.during.lockin.period", defaultUserMessage, "transactionDate", + transactionDTO.getTransactionDate().format(transactionDTO.getFormatter()), + getLockedInUntilDate().format(transactionDTO.getFormatter())); + + final List dataValidationErrors = new ArrayList<>(); + dataValidationErrors.add(error); + + throw new PlatformApiDataValidationException(dataValidationErrors); + } + validatePivotDateTransaction(transactionDTO.getTransactionDate(), backdatedTxnsAllowedTill, relaxingDaysConfigForPivotDate, + "savingsaccount"); + validateActivityNotBeforeClientOrGroupTransferDate(SavingsEvent.SAVINGS_WITHDRAWAL, transactionDTO.getTransactionDate()); + + if (applyWithdrawFee) { + // auto pay withdrawal fee + payWithdrawalFee(transactionDTO.getTransactionAmount(), transactionDTO.getTransactionDate(), transactionDTO.getPaymentDetail(), + backdatedTxnsAllowedTill, refNo); + } + + final Money transactionAmountMoney = Money.of(this.currency, transactionDTO.getTransactionAmount()); + final SavingsAccountTransaction transaction = SavingsAccountTransaction.withdrawal(this, office(), + transactionDTO.getPaymentDetail(), transactionDTO.getTransactionDate(), transactionAmountMoney, refNo); + + if (backdatedTxnsAllowedTill) { + addTransactionToExisting(transaction); + } else { + this.accrualsForSavingsReverse(transactionDTO, backdatedTxnsAllowedTill); + addTransaction(transaction); + } + + if (this.sub_status.equals(SavingsAccountSubStatusEnum.INACTIVE.getValue()) + || this.sub_status.equals(SavingsAccountSubStatusEnum.DORMANT.getValue())) { + this.sub_status = SavingsAccountSubStatusEnum.NONE.getValue(); + } + if (backdatedTxnsAllowedTill) { + this.summary.updateSummaryWithPivotConfig(this.currency, this.savingsAccountTransactionSummaryWrapper, transaction, + this.savingsAccountTransactions); + } + return transaction; + } + + public BigDecimal calculateWithdrawalFee(final BigDecimal transactionAmount) { + BigDecimal result = BigDecimal.ZERO; + if (isWithdrawalFeeApplicableForTransfer()) { + for (SavingsAccountCharge charge : this.charges()) { + if (charge.isWithdrawalFee() && charge.isActive()) { + result = result.add(charge.calculateWithdralFeeAmount(transactionAmount), MoneyHelper.getMathContext()); + } + } + } + return result; + } + + private void payWithdrawalFee(final BigDecimal transactionAmount, final LocalDate transactionDate, final PaymentDetail paymentDetail, + final boolean backdatedTxnsAllowedTill, final String refNo) { + for (SavingsAccountCharge charge : this.charges()) { + if (charge.isWithdrawalFee() && charge.isActive()) { + + if (charge.getFreeWithdrawalCount() == null) { + charge.setFreeWithdrawalCount(0); + } + + if (charge.isEnablePaymentType() && charge.isEnableFreeWithdrawal()) { // discount transaction to + // specific paymentType + if (paymentDetail != null && paymentDetail.getPaymentType() != null + && paymentDetail.getPaymentType().getName().equals(charge.getCharge().getPaymentType().getName())) { + resetFreeChargeDaysCount(charge, transactionAmount, transactionDate, refNo); + } + } else if (charge.isEnablePaymentType()) { // normal charge-transaction to specific paymentType + if (paymentDetail != null && paymentDetail.getPaymentType() != null + && paymentDetail.getPaymentType().getName().equals(charge.getCharge().getPaymentType().getName())) { + charge.updateWithdralFeeAmount(transactionAmount); + this.payCharge(charge, charge.getAmountOutstanding(this.getCurrency()), transactionDate, backdatedTxnsAllowedTill, + refNo); + } + } else if (!charge.isEnablePaymentType() && charge.isEnableFreeWithdrawal()) { // discount transaction + // irrespective of + // PaymentTypes. + resetFreeChargeDaysCount(charge, transactionAmount, transactionDate, refNo); + + } else { // normal-withdraw + charge.updateWithdralFeeAmount(transactionAmount); + this.payCharge(charge, charge.getAmountOutstanding(this.getCurrency()), transactionDate, backdatedTxnsAllowedTill, + refNo); + } + } + } + } + + private void resetFreeChargeDaysCount(SavingsAccountCharge charge, final BigDecimal transactionAmount, final LocalDate transactionDate, + final String refNo) { + LocalDate resetDate = charge.getResetChargeDate(); + + Integer restartPeriod = charge.getRestartFrequency(); + if (charge.getRestartFrequencyEnum() == 2) { // calculation for months + LocalDate localDate = DateUtils.getBusinessLocalDate(); + + LocalDate resetLocalDate; + if (resetDate == null) { + resetLocalDate = this.activatedOnDate; + } else { + resetLocalDate = resetDate; + } + + LocalDate gapIntervalMonth = resetLocalDate.plusMonths(restartPeriod); + + YearMonth gapYearMonth = YearMonth.from(gapIntervalMonth); + YearMonth localYearMonth = YearMonth.from(localDate); + if (localYearMonth.isBefore(gapYearMonth)) { + countValidation(charge, transactionAmount, transactionDate, refNo); + } else { + discountCharge(1, charge); + } + } else { // calculation for days + long completedDays; + + if (resetDate == null) { + completedDays = DAYS.between(DateUtils.getBusinessLocalDate(), this.activatedOnDate); + + } else { + completedDays = DAYS.between(DateUtils.getBusinessLocalDate(), resetDate); + } + + int totalDays = (int) completedDays; + + if (totalDays < restartPeriod) { + countValidation(charge, transactionAmount, transactionDate, refNo); + } else { + discountCharge(1, charge); + } + } + } + + private void countValidation(SavingsAccountCharge charge, final BigDecimal transactionAmount, final LocalDate transactionDate, + final String refNo) { + boolean backdatedTxnsAllowedTill = false; + if (charge.getFreeWithdrawalCount() < charge.getFrequencyFreeWithdrawalCharge()) { + final Integer count = charge.getFreeWithdrawalCount() + 1; + charge.setFreeWithdrawalCount(count); + charge.updateNoWithdrawalFee(); + } else { + charge.updateWithdralFeeAmount(transactionAmount); + this.payCharge(charge, charge.getAmountOutstanding(this.getCurrency()), transactionDate, backdatedTxnsAllowedTill, refNo); + } + } + + private void discountCharge(Integer freeWithdrawalCount, SavingsAccountCharge charge) { + charge.setFreeWithdrawalCount(freeWithdrawalCount); + charge.setDiscountDueDate(DateUtils.getBusinessLocalDate()); + charge.updateNoWithdrawalFee(); + } + + public boolean isBeforeLastPostingPeriod(final LocalDate transactionDate, final boolean backdatedTxnsAllowedTill) { + + boolean transactionBeforeLastInterestPosting = false; + + if (!backdatedTxnsAllowedTill) { + for (final SavingsAccountTransaction transaction : retrieveListOfTransactions()) { + if ((transaction.isInterestPostingAndNotReversed() || transaction.isOverdraftInterestAndNotReversed()) + && transaction.isAfter(transactionDate) && !transaction.isReversalTransaction()) { + transactionBeforeLastInterestPosting = true; + break; + } + } + } else { + if (this.summary.getInterestPostedTillDate() == null) { + return false; + } + transactionBeforeLastInterestPosting = DateUtils.isBefore(transactionDate, this.summary.getInterestPostedTillDate()); + } + + return transactionBeforeLastInterestPosting; + } + + public void validateAccountBalanceDoesNotBecomeNegative(final BigDecimal transactionAmount, final boolean isException, + final List depositAccountOnHoldTransactions, final boolean backdatedTxnsAllowedTill) { + + List transactionsSortedByDate = null; + + if (backdatedTxnsAllowedTill) { + transactionsSortedByDate = retrieveSortedTransactions(); + } else { + transactionsSortedByDate = retrieveListOfTransactions(); + } + + Money runningBalance = Money.zero(this.currency); + if (backdatedTxnsAllowedTill) { + runningBalance = Money.of(this.currency, this.summary.getRunningBalanceOnPivotDate()); + } + + Money minRequiredBalance = minRequiredBalanceDerived(getCurrency()); + LocalDate lastSavingsDate = null; + final BigDecimal withdrawalFee = null; + for (final SavingsAccountTransaction transaction : transactionsSortedByDate) { + if (transaction.isNotReversed() && transaction.isCredit() && !transaction.isReversalTransaction()) { + runningBalance = runningBalance.plus(transaction.getAmount(this.currency)); + } else if (transaction.isNotReversed() && transaction.isDebit() && !transaction.isReversalTransaction()) { + runningBalance = runningBalance.minus(transaction.getAmount(this.currency)); + } else { + continue; + } + + /* + * Loop through the onHold funds and see if we need to deduct or add to minimum required balance and the + * point in time the transaction was made: + */ + if (depositAccountOnHoldTransactions != null) { + for (final DepositAccountOnHoldTransaction onHoldTransaction : depositAccountOnHoldTransactions) { + // Compare the balance of the on hold: + if (!DateUtils.isAfter(onHoldTransaction.getTransactionDate(), transaction.getTransactionDate()) + && (lastSavingsDate == null || DateUtils.isAfter(onHoldTransaction.getTransactionDate(), lastSavingsDate))) { + if (onHoldTransaction.getTransactionType().isHold()) { + minRequiredBalance = minRequiredBalance.plus(onHoldTransaction.getAmount(this.currency)); + } else { + minRequiredBalance = minRequiredBalance.minus(onHoldTransaction.getAmount(this.currency)); + } + } + } + } + + // deal with potential minRequiredBalance and + // enforceMinRequiredBalance + if (!isException && transaction.canProcessBalanceCheck() && !isOverdraft()) { + if (runningBalance.minus(minRequiredBalance).isLessThanZero()) { + throw new InsufficientAccountBalanceException("transactionAmount", getAccountBalance(), withdrawalFee, + transactionAmount); + } + } + lastSavingsDate = transaction.getTransactionDate(); + + } + + // In overdraft cases, minRequiredBalance can be in violation after + // interest posting + // and should be checked after processing all transactions + if (isOverdraft()) { + if (runningBalance.minus(minRequiredBalance).isLessThanZero()) { + throw new InsufficientAccountBalanceException("transactionAmount", getAccountBalance(), withdrawalFee, transactionAmount); + } + } + + if (this.getSavingsHoldAmount().compareTo(BigDecimal.ZERO) > 0) { + if (this.enforceMinRequiredBalance) { + if (runningBalance.minus(minRequiredBalance.plus(this.getSavingsHoldAmount())).isLessThanZero()) { + throw new InsufficientAccountBalanceException("transactionAmount", getAccountBalance(), withdrawalFee, + transactionAmount); + } + } else { + if (runningBalance.minus(this.getSavingsHoldAmount()).isLessThanZero()) { + throw new InsufficientAccountBalanceException("transactionAmount", getAccountBalance(), withdrawalFee, + transactionAmount); + } + } + } + } + + public void validateAccountBalanceDoesNotBecomeNegative(final String transactionAction, + final List depositAccountOnHoldTransactions, final boolean backdatedTxnsAllowedTill) { + + List transactionsSortedByDate = null; + BigDecimal transactionAmount = null; + if (backdatedTxnsAllowedTill) { + transactionsSortedByDate = retrieveSortedTransactions(); + } else { + transactionsSortedByDate = retrieveListOfTransactions(); + } + Money runningBalance = Money.zero(this.currency); + + if (backdatedTxnsAllowedTill) { + runningBalance = Money.of(this.currency, this.summary.getRunningBalanceOnPivotDate()); + } + + Money minRequiredBalance = minRequiredBalanceDerived(getCurrency()); + LocalDate lastSavingsDate = null; + for (final SavingsAccountTransaction transaction : transactionsSortedByDate) { + + transactionAmount = transaction.getAmount(); + + if (transaction.isNotReversed() && transaction.isCredit()) { + runningBalance = runningBalance.plus(transaction.getAmount(this.currency)); + } else if (transaction.isNotReversed() && transaction.isDebit()) { + runningBalance = runningBalance.minus(transaction.getAmount(this.currency)); + } + + /* + * Loop through the onHold funds and see if we need to deduct or add to minimum required balance and the + * point in time the transaction was made: + */ + if (depositAccountOnHoldTransactions != null) { + for (final DepositAccountOnHoldTransaction onHoldTransaction : depositAccountOnHoldTransactions) { + // Compare the balance of the on hold: + if (!DateUtils.isAfter(onHoldTransaction.getTransactionDate(), transaction.getTransactionDate()) + && (lastSavingsDate == null || DateUtils.isAfter(onHoldTransaction.getTransactionDate(), lastSavingsDate))) { + if (onHoldTransaction.getTransactionType().isHold()) { + minRequiredBalance = minRequiredBalance.plus(onHoldTransaction.getAmount(this.currency)); + } else { + minRequiredBalance = minRequiredBalance.minus(onHoldTransaction.getAmount(this.currency)); + } + } + } + } + + // enforceMinRequiredBalance + if (transaction.canProcessBalanceCheck()) { + if (runningBalance.minus(minRequiredBalance).isLessThanZero()) { + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(depositAccountType().resourceName() + transactionAction); + if (!this.allowOverdraft) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("results.in.balance.going.negative"); + } + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + } + lastSavingsDate = transaction.getTransactionDate(); + } + + BigDecimal withdrawalFee = null; + if (isOverdraft()) { + if (runningBalance.minus(minRequiredBalance).isLessThanZero()) { + throw new InsufficientAccountBalanceException("transactionAmount", getAccountBalance(), withdrawalFee, transactionAmount); + } + } + } + + public void validateAccountBalanceDoesNotViolateOverdraft(final List savingsAccountTransaction, + final BigDecimal amountPaid) { + if (savingsAccountTransaction != null && savingsAccountTransaction.size() > 0) { + SavingsAccountTransaction savingsAccountTransactionFirst = savingsAccountTransaction.get(0); + if (!this.allowOverdraft) { + if (savingsAccountTransactionFirst.getRunningBalance(this.currency).minus(amountPaid).isLessThanZero()) { + throw new InsufficientAccountBalanceException("transactionAmount", getAccountBalance(), null, amountPaid); + } + } + + } else { + if (!this.allowOverdraft) { + throw new InsufficientAccountBalanceException("transactionAmount", BigDecimal.ZERO, null, amountPaid); + } + } + } + + protected boolean isAccountLocked(final LocalDate transactionDate) { + return DateUtils.isBefore(transactionDate, getLockedInUntilDate()); + } + + public LocalDate getLockedInUntilDate() { + return this.lockedInUntilDate; + } + + public BigDecimal getAccountBalance() { + return this.summary.getAccountBalance(this.currency).getAmount(); + } + + public void modifyApplication(final JsonCommand command, final Map actualChanges) { + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(SAVINGS_ACCOUNT_RESOURCE_NAME + SavingsApiConstants.modifyApplicationAction); + this.modifyApplication(command, actualChanges, baseDataValidator); + + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + public void modifyApplication(final JsonCommand command, final Map actualChanges, + final DataValidatorBuilder baseDataValidator) { + + final SavingsAccountStatusType currentStatus = SavingsAccountStatusType.fromInt(this.status); + if (!SavingsAccountStatusType.SUBMITTED_AND_PENDING_APPROVAL.hasStateOf(currentStatus)) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("not.in.submittedandpendingapproval.state"); + return; + } + + final String localeAsInput = command.locale(); + final String dateFormat = command.dateFormat(); + + if (command.isChangeInLocalDateParameterNamed(SavingsApiConstants.submittedOnDateParamName, getSubmittedOnDate())) { + final String newValueAsString = command.stringValueOfParameterNamed(SavingsApiConstants.submittedOnDateParamName); + actualChanges.put(SavingsApiConstants.submittedOnDateParamName, newValueAsString); + actualChanges.put(SavingsApiConstants.localeParamName, localeAsInput); + actualChanges.put(SavingsApiConstants.dateFormatParamName, dateFormat); + this.submittedOnDate = command.localDateValueOfParameterNamed(SavingsApiConstants.submittedOnDateParamName); + } + if (command.isChangeInStringParameterNamed(SavingsApiConstants.accountNoParamName, this.accountNumber)) { + final String newValue = command.stringValueOfParameterNamed(SavingsApiConstants.accountNoParamName); + actualChanges.put(SavingsApiConstants.accountNoParamName, newValue); + this.accountNumber = StringUtils.defaultIfEmpty(newValue, null); + } + if (command.isChangeInStringParameterNamed(SavingsApiConstants.externalIdParamName, this.externalId.getValue())) { + final String newValue = command.stringValueOfParameterNamed(SavingsApiConstants.externalIdParamName); + actualChanges.put(SavingsApiConstants.externalIdParamName, newValue); + this.externalId = ExternalIdFactory.produce(newValue); + if (this.externalId.isEmpty() && TemporaryConfigurationServiceContainer.isExternalIdAutoGenerationEnabled()) { + this.externalId = ExternalId.generate(); + } + } + if (command.isChangeInLongParameterNamed(SavingsApiConstants.clientIdParamName, clientId())) { + final Long newValue = command.longValueOfParameterNamed(SavingsApiConstants.clientIdParamName); + actualChanges.put(SavingsApiConstants.clientIdParamName, newValue); + } + if (command.isChangeInLongParameterNamed(SavingsApiConstants.groupIdParamName, groupId())) { + final Long newValue = command.longValueOfParameterNamed(SavingsApiConstants.groupIdParamName); + actualChanges.put(SavingsApiConstants.groupIdParamName, newValue); + } + if (command.isChangeInLongParameterNamed(SavingsApiConstants.productIdParamName, this.product.getId())) { + final Long newValue = command.longValueOfParameterNamed(SavingsApiConstants.productIdParamName); + actualChanges.put(SavingsApiConstants.productIdParamName, newValue); + } + if (command.isChangeInLongParameterNamed(SavingsApiConstants.fieldOfficerIdParamName, hasSavingsOfficerId())) { + final Long newValue = command.longValueOfParameterNamed(SavingsApiConstants.fieldOfficerIdParamName); + actualChanges.put(SavingsApiConstants.fieldOfficerIdParamName, newValue); + } + if (command.isChangeInBigDecimalParameterNamed(SavingsApiConstants.nominalAnnualInterestRateParamName, + this.nominalAnnualInterestRate)) { + final BigDecimal newValue = command.bigDecimalValueOfParameterNamed(SavingsApiConstants.nominalAnnualInterestRateParamName); + actualChanges.put(SavingsApiConstants.nominalAnnualInterestRateParamName, newValue); + this.nominalAnnualInterestRate = newValue; + } + if (command.isChangeInIntegerParameterNamed(SavingsApiConstants.interestCompoundingPeriodTypeParamName, + this.interestCompoundingPeriodType)) { + final Integer newValue = command.integerValueOfParameterNamed(SavingsApiConstants.interestCompoundingPeriodTypeParamName); + this.interestCompoundingPeriodType = newValue != null ? SavingsCompoundingInterestPeriodType.fromInt(newValue).getValue() + : newValue; + actualChanges.put(SavingsApiConstants.interestCompoundingPeriodTypeParamName, this.interestCompoundingPeriodType); + } + if (command.isChangeInIntegerParameterNamed(SavingsApiConstants.interestPostingPeriodTypeParamName, + this.interestPostingPeriodType)) { + final Integer newValue = command.integerValueOfParameterNamed(SavingsApiConstants.interestPostingPeriodTypeParamName); + this.interestPostingPeriodType = newValue != null ? SavingsPostingInterestPeriodType.fromInt(newValue).getValue() : newValue; + actualChanges.put(SavingsApiConstants.interestPostingPeriodTypeParamName, this.interestPostingPeriodType); + } + if (command.isChangeInIntegerParameterNamed(SavingsApiConstants.interestCalculationTypeParamName, this.interestCalculationType)) { + final Integer newValue = command.integerValueOfParameterNamed(SavingsApiConstants.interestCalculationTypeParamName); + this.interestCalculationType = newValue != null ? SavingsInterestCalculationType.fromInt(newValue).getValue() : newValue; + actualChanges.put(SavingsApiConstants.interestCalculationTypeParamName, this.interestCalculationType); + } + if (command.isChangeInIntegerParameterNamed(SavingsApiConstants.interestCalculationDaysInYearTypeParamName, + this.interestCalculationDaysInYearType)) { + final Integer newValue = command.integerValueOfParameterNamed(SavingsApiConstants.interestCalculationDaysInYearTypeParamName); + this.interestCalculationDaysInYearType = newValue != null + ? SavingsInterestCalculationDaysInYearType.fromInt(newValue).getValue() + : newValue; + actualChanges.put(SavingsApiConstants.interestCalculationDaysInYearTypeParamName, this.interestCalculationDaysInYearType); + } + if (command.isChangeInBigDecimalParameterNamedDefaultingZeroToNull(SavingsApiConstants.minRequiredOpeningBalanceParamName, + this.minRequiredOpeningBalance)) { + final BigDecimal newValue = command + .bigDecimalValueOfParameterNamedDefaultToNullIfZero(SavingsApiConstants.minRequiredOpeningBalanceParamName); + actualChanges.put(SavingsApiConstants.minRequiredOpeningBalanceParamName, newValue); + actualChanges.put("locale", localeAsInput); + this.minRequiredOpeningBalance = Money.of(this.currency, newValue).getAmount(); + } + if (command.isChangeInIntegerParameterNamedDefaultingZeroToNull(SavingsApiConstants.lockinPeriodFrequencyParamName, + this.lockinPeriodFrequency)) { + final Integer newValue = command + .integerValueOfParameterNamedDefaultToNullIfZero(SavingsApiConstants.lockinPeriodFrequencyParamName); + actualChanges.put(SavingsApiConstants.lockinPeriodFrequencyParamName, newValue); + actualChanges.put("locale", localeAsInput); + this.lockinPeriodFrequency = newValue; + } + + if (command.isChangeInIntegerParameterNamed(SavingsApiConstants.lockinPeriodFrequencyTypeParamName, + this.lockinPeriodFrequencyType)) { + final Integer newValue = command.integerValueOfParameterNamed(SavingsApiConstants.lockinPeriodFrequencyTypeParamName); + actualChanges.put(SavingsApiConstants.lockinPeriodFrequencyTypeParamName, newValue); + this.lockinPeriodFrequencyType = newValue != null ? SavingsPeriodFrequencyType.fromInt(newValue).getValue() : newValue; + } + + // set period type to null if frequency is null + if (this.lockinPeriodFrequency == null) { + this.lockinPeriodFrequencyType = null; + } + + if (command.isChangeInBooleanParameterNamed(withdrawalFeeForTransfersParamName, this.withdrawalFeeApplicableForTransfer)) { + final boolean newValue = command.booleanPrimitiveValueOfParameterNamed(withdrawalFeeForTransfersParamName); + actualChanges.put(withdrawalFeeForTransfersParamName, newValue); + this.withdrawalFeeApplicableForTransfer = newValue; + } + + // charges + final String chargesParamName = "charges"; + if (command.hasParameter(chargesParamName)) { + final JsonArray jsonArray = command.arrayOfParameterNamed(chargesParamName); + if (jsonArray != null) { + actualChanges.put(chargesParamName, command.jsonFragment(chargesParamName)); + } + } + + if (command.isChangeInBooleanParameterNamed(allowOverdraftParamName, this.allowOverdraft)) { + final boolean newValue = command.booleanPrimitiveValueOfParameterNamed(allowOverdraftParamName); + actualChanges.put(allowOverdraftParamName, newValue); + this.allowOverdraft = newValue; + } + + if (command.isChangeInBigDecimalParameterNamedDefaultingZeroToNull(overdraftLimitParamName, this.overdraftLimit)) { + final BigDecimal newValue = command.bigDecimalValueOfParameterNamedDefaultToNullIfZero(overdraftLimitParamName); + actualChanges.put(overdraftLimitParamName, newValue); + actualChanges.put(localeParamName, localeAsInput); + this.overdraftLimit = newValue; + } + + if (command.isChangeInBigDecimalParameterNamedDefaultingZeroToNull(nominalAnnualInterestRateOverdraftParamName, + this.nominalAnnualInterestRateOverdraft)) { + final BigDecimal newValue = command + .bigDecimalValueOfParameterNamedDefaultToNullIfZero(nominalAnnualInterestRateOverdraftParamName); + actualChanges.put(nominalAnnualInterestRateOverdraftParamName, newValue); + actualChanges.put(localeParamName, localeAsInput); + this.nominalAnnualInterestRateOverdraft = newValue; + } + + if (command.isChangeInBigDecimalParameterNamedDefaultingZeroToNull(minOverdraftForInterestCalculationParamName, + this.minOverdraftForInterestCalculation)) { + final BigDecimal newValue = command + .bigDecimalValueOfParameterNamedDefaultToNullIfZero(minOverdraftForInterestCalculationParamName); + actualChanges.put(minOverdraftForInterestCalculationParamName, newValue); + actualChanges.put(localeParamName, localeAsInput); + this.minOverdraftForInterestCalculation = newValue; + } + + if (!this.allowOverdraft) { + this.overdraftLimit = null; + this.nominalAnnualInterestRateOverdraft = null; + this.minOverdraftForInterestCalculation = null; + } + + if (command.isChangeInBooleanParameterNamed(enforceMinRequiredBalanceParamName, this.enforceMinRequiredBalance)) { + final boolean newValue = command.booleanPrimitiveValueOfParameterNamed(enforceMinRequiredBalanceParamName); + actualChanges.put(enforceMinRequiredBalanceParamName, newValue); + this.enforceMinRequiredBalance = newValue; + } + if (command.isChangeInBigDecimalParameterNamedDefaultingZeroToNull(minRequiredBalanceParamName, this.minRequiredBalance)) { + final BigDecimal newValue = command.bigDecimalValueOfParameterNamedDefaultToNullIfZero(minRequiredBalanceParamName); + actualChanges.put(minRequiredBalanceParamName, newValue); + actualChanges.put(localeParamName, localeAsInput); + this.minRequiredBalance = newValue; + } + if (command.isChangeInBooleanParameterNamed(lienAllowedParamName, this.lienAllowed)) { + final boolean newValue = command.booleanPrimitiveValueOfParameterNamed(lienAllowedParamName); + actualChanges.put(lienAllowedParamName, newValue); + this.lienAllowed = newValue; + } + if (command.isChangeInBigDecimalParameterNamedDefaultingZeroToNull(maxAllowedLienLimitParamName, this.maxAllowedLienLimit)) { + final BigDecimal newValue = command.bigDecimalValueOfParameterNamedDefaultToNullIfZero(maxAllowedLienLimitParamName); + actualChanges.put(maxAllowedLienLimitParamName, newValue); + actualChanges.put(localeParamName, localeAsInput); + this.maxAllowedLienLimit = newValue; + } + if (command.isChangeInBooleanParameterNamed(withHoldTaxParamName, this.withHoldTax)) { + final boolean newValue = command.booleanPrimitiveValueOfParameterNamed(withHoldTaxParamName); + actualChanges.put(withHoldTaxParamName, newValue); + this.withHoldTax = newValue; + if (this.withHoldTax && this.taxGroup == null) { + baseDataValidator.reset().parameter(withHoldTaxParamName).failWithCode("not.supported.for.this.account"); + } + } + validateLockinDetails(baseDataValidator); + esnureOverdraftLimitsSetForOverdraftAccounts(); + } + + /** + * If overdrafts are allowed and the overdraft limit is not set, set the same to Zero + **/ + private void esnureOverdraftLimitsSetForOverdraftAccounts() { + + this.overdraftLimit = this.overdraftLimit == null ? BigDecimal.ZERO : this.overdraftLimit; + this.nominalAnnualInterestRateOverdraft = this.nominalAnnualInterestRateOverdraft == null ? BigDecimal.ZERO + : this.nominalAnnualInterestRateOverdraft; + this.minOverdraftForInterestCalculation = this.minOverdraftForInterestCalculation == null ? BigDecimal.ZERO + : this.minOverdraftForInterestCalculation; + } + + private void validateLockinDetails(final DataValidatorBuilder baseDataValidator) { + + /* + * final List dataValidationErrors = new ArrayList(); final + * DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + * .resource(resourceName); + */ + + if (this.lockinPeriodFrequency == null) { + baseDataValidator.reset().parameter(lockinPeriodFrequencyTypeParamName).value(this.lockinPeriodFrequencyType).ignoreIfNull() + .inMinMaxRange(0, 3); + + if (this.lockinPeriodFrequencyType != null) { + baseDataValidator.reset().parameter(lockinPeriodFrequencyParamName).value(this.lockinPeriodFrequency).ignoreIfNull() + .integerZeroOrGreater(); + } + } else { + baseDataValidator.reset().parameter(lockinPeriodFrequencyParamName).value(this.lockinPeriodFrequencyType) + .integerZeroOrGreater(); + baseDataValidator.reset().parameter(lockinPeriodFrequencyTypeParamName).value(this.lockinPeriodFrequencyType).ignoreIfNull() + .inMinMaxRange(0, 3); + } + } + + public Map deriveAccountingBridgeData(final String currencyCode, final Set existingTransactionIds, + final Set existingReversedTransactionIds, boolean isAccountTransfer, final boolean backdatedTxnsAllowedTill) { + + final Map accountingBridgeData = new LinkedHashMap<>(); + accountingBridgeData.put("savingsId", getId()); + accountingBridgeData.put("savingsProductId", productId()); + accountingBridgeData.put("currencyCode", currencyCode); + accountingBridgeData.put("officeId", officeId()); + accountingBridgeData.put("cashBasedAccountingEnabled", isCashBasedAccountingEnabledOnSavingsProduct()); + accountingBridgeData.put("accrualBasedAccountingEnabled", isAccrualBasedAccountingEnabledOnSavingsProduct()); + accountingBridgeData.put("isAccountTransfer", isAccountTransfer); + accountingBridgeData.put("depositAccountType", depositAccountType().getValue()); + + final List> newSavingsTransactions = new ArrayList<>(); + + List trans = null; + + if (backdatedTxnsAllowedTill) { + trans = getSavingsAccountTransactionsWithPivotConfig(); + } else { + trans = getTransactions(); + } + + // Adding new transactions to the array + for (final SavingsAccountTransaction transaction : trans) { + if (transaction.isReversed() && !existingReversedTransactionIds.contains(transaction.getId())) { + newSavingsTransactions.add(transaction.toMapData(currencyCode)); + } else if (!existingTransactionIds.contains(transaction.getId())) { + newSavingsTransactions.add(transaction.toMapData(currencyCode)); + } + } + + accountingBridgeData.put("newSavingsTransactions", newSavingsTransactions); + return accountingBridgeData; + } + + public Collection findExistingTransactionIds() { + final Collection ids = new ArrayList<>(); + List trans = getTransactions(); + for (final SavingsAccountTransaction transaction : trans) { + ids.add(transaction.getId()); + } + return ids; + } + + public Collection findCurrentTransactionIdsWithPivotDateConfig() { + + final Collection ids = new ArrayList<>(); + List trans = getSavingsAccountTransactionsWithPivotConfig(); + for (final SavingsAccountTransaction transaction : trans) { + ids.add(transaction.getId()); + } + return ids; + } + + public Collection findExistingReversedTransactionIds() { + + final Collection ids = new ArrayList<>(); + List trans = getTransactions(); + // time consuming + for (final SavingsAccountTransaction transaction : trans) { + if (transaction.isReversed()) { + ids.add(transaction.getId()); + } + } + + return ids; + } + + public Collection findCurrentReversedTransactionIdsWithPivotDateConfig() { + final Collection ids = new ArrayList<>(); + List trans = getSavingsAccountTransactionsWithPivotConfig(); + // time consuming + for (final SavingsAccountTransaction transaction : trans) { + if (transaction.isReversed()) { + ids.add(transaction.getId()); + } + } + return ids; + } + + public void update(final Client client) { + this.client = client; + } + + public void update(final Group group) { + this.group = group; + } + + public void update(final SavingsProduct product) { + this.product = product; + this.minBalanceForInterestCalculation = product.minBalanceForInterestCalculation(); + } + + public void update(final Staff savingsOfficer) { + this.savingsOfficer = savingsOfficer; + } + + public void updateAccountNo(final String newAccountNo) { + this.accountNumber = newAccountNo; + this.accountNumberRequiresAutoGeneration = false; + } + + public boolean isAccountNumberRequiresAutoGeneration() { + return this.accountNumberRequiresAutoGeneration; + } + + public Long productId() { + return this.product.getId(); + } + + public SavingsProduct savingsProduct() { + return this.product; + } + + private Boolean isCashBasedAccountingEnabledOnSavingsProduct() { + return this.product.isCashBasedAccountingEnabled(); + } + + private Boolean isAccrualBasedAccountingEnabledOnSavingsProduct() { + return this.product.isAccrualBasedAccountingEnabled(); + } + + public Long officeId() { + Long officeId = null; + if (this.client != null) { + officeId = this.client.officeId(); + } else if (this.group != null) { + officeId = this.group.officeId(); + } + return officeId; + } + + public Office office() { + Office office = null; + if (this.client != null) { + office = this.client.getOffice(); + } else if (this.group != null) { + office = this.group.getOffice(); + } + return office; + } + + public Staff getSavingsOfficer() { + return this.savingsOfficer; + } + + public Boolean getEnforceMinRequiredBalance() { + return this.enforceMinRequiredBalance; + } + + public Boolean getLienAllowed() { + return this.lienAllowed; + } + + public void unassignSavingsOfficer() { + this.savingsOfficer = null; + } + + public void assignSavingsOfficer(final Staff fieldOfficer) { + this.savingsOfficer = fieldOfficer; + } + + public Long clientId() { + Long id = null; + if (this.client != null) { + id = this.client.getId(); + } + return id; + } + + public Long groupId() { + Long id = null; + if (this.group != null) { + id = this.group.getId(); + } + return id; + } + + public GroupSavingsIndividualMonitoring getGsim() { + return gsim; + } + + public Long getSavingsProductId() { + return this.savingsProduct().getId(); + } + + public void setGsim(GroupSavingsIndividualMonitoring gsim) { + this.gsim = gsim; + } + + public Long hasSavingsOfficerId() { + Long id = null; + if (this.savingsOfficer != null) { + id = this.savingsOfficer.getId(); + } + return id; + } + + public boolean hasSavingsOfficer(final Staff fromSavingsOfficer) { + + boolean matchesCurrentSavingsOfficer = false; + if (this.savingsOfficer != null) { + matchesCurrentSavingsOfficer = this.savingsOfficer.identifiedBy(fromSavingsOfficer); + } else { + matchesCurrentSavingsOfficer = fromSavingsOfficer == null; + } + return matchesCurrentSavingsOfficer; + } + + public void reassignSavingsOfficer(final Staff newSavingsOfficer, final LocalDate assignmentDate) { + final SavingsOfficerAssignmentHistory latestHistoryRecord = findLatestIncompleteHistoryRecord(); + final SavingsOfficerAssignmentHistory lastAssignmentRecord = findLastAssignmentHistoryRecord(newSavingsOfficer); + + // assignment date should not be less than savings account submitted date + if (DateUtils.isBefore(assignmentDate, getSubmittedOnDate())) { + final String errorMessage = "The Savings Officer assignment date (" + assignmentDate.toString() + + ") cannot be before savings submitted date (" + getSubmittedOnDate().toString() + ")."; + throw new SavingsOfficerAssignmentDateException("cannot.be.before.savings.submitted.date", errorMessage, assignmentDate, + getSubmittedOnDate()); + } else if (lastAssignmentRecord != null && lastAssignmentRecord.isBeforeEndDate(assignmentDate)) { + final String errorMessage = "The Savings Officer assignment date (" + assignmentDate + + ") cannot be before previous Savings Officer unassigned date (" + lastAssignmentRecord.getEndDate() + ")."; + throw new SavingsOfficerAssignmentDateException("cannot.be.before.previous.unassignement.date", errorMessage, assignmentDate, + lastAssignmentRecord.getEndDate()); + } else if (DateUtils.isDateInTheFuture(assignmentDate)) { + final String errorMessage = "The Savings Officer assignment date (" + assignmentDate + ") cannot be in the future."; + throw new SavingsOfficerAssignmentDateException("cannot.be.a.future.date", errorMessage, assignmentDate); + } else if (latestHistoryRecord != null && this.savingsOfficer.identifiedBy(newSavingsOfficer)) { + latestHistoryRecord.setStartDate(assignmentDate); + } else if (latestHistoryRecord != null && latestHistoryRecord.matchesStartDateOf(assignmentDate)) { + latestHistoryRecord.setSavingsOfficer(newSavingsOfficer); + this.savingsOfficer = newSavingsOfficer; + } else if (latestHistoryRecord != null && latestHistoryRecord.isBeforeStartDate(assignmentDate)) { + final String errorMessage = "Savings account with identifier " + getId() + " was already assigned before date " + + assignmentDate; + throw new SavingsOfficerAssignmentDateException("is.before.last.assignment.date", errorMessage, getId(), assignmentDate); + } else { + if (latestHistoryRecord != null) { + // savings officer correctly changed from previous savings + // officer to + // new savings officer + latestHistoryRecord.setEndDate(assignmentDate); + } + this.savingsOfficer = newSavingsOfficer; + if (isNotSubmittedAndPendingApproval()) { + final SavingsOfficerAssignmentHistory savingsOfficerAssignmentHistory = SavingsOfficerAssignmentHistory.createNew(this, + this.savingsOfficer, assignmentDate); + this.savingsOfficerHistory.add(savingsOfficerAssignmentHistory); + } + } + } + + private SavingsOfficerAssignmentHistory findLastAssignmentHistoryRecord(final Staff newSavingsOfficer) { + SavingsOfficerAssignmentHistory lastAssignmentRecordLatestEndDate = null; + for (final SavingsOfficerAssignmentHistory historyRecord : this.savingsOfficerHistory) { + if (historyRecord.isCurrentRecord() && !historyRecord.isSameSavingsOfficer(newSavingsOfficer)) { + lastAssignmentRecordLatestEndDate = historyRecord; + break; + } + + if (lastAssignmentRecordLatestEndDate == null) { + lastAssignmentRecordLatestEndDate = historyRecord; + } else if (historyRecord.isBeforeEndDate(lastAssignmentRecordLatestEndDate.getEndDate()) + && !historyRecord.isSameSavingsOfficer(newSavingsOfficer)) { + lastAssignmentRecordLatestEndDate = historyRecord; + } + } + return lastAssignmentRecordLatestEndDate; + } + + public LocalDate getSubmittedOnDate() { + return submittedOnDate; + } + + public LocalDate getApprovedOnDate() { + return approvedOnDate; + } + + public LocalDate getRejectedOnDate() { + return rejectedOnDate; + } + + public AppUser getRejectedBy() { + return this.rejectedBy; + } + + public void removeSavingsOfficer(final LocalDate unassignDate) { + final SavingsOfficerAssignmentHistory latestHistoryRecord = findLatestIncompleteHistoryRecord(); + + if (latestHistoryRecord != null) { + validateUnassignDate(latestHistoryRecord, unassignDate); + latestHistoryRecord.setEndDate(unassignDate); + } + this.savingsOfficer = null; + } + + private SavingsOfficerAssignmentHistory findLatestIncompleteHistoryRecord() { + SavingsOfficerAssignmentHistory latestRecordWithNoEndDate = null; + for (final SavingsOfficerAssignmentHistory historyRecord : this.savingsOfficerHistory) { + if (historyRecord.isCurrentRecord()) { + latestRecordWithNoEndDate = historyRecord; + break; + } + } + return latestRecordWithNoEndDate; + } + + private void validateUnassignDate(final SavingsOfficerAssignmentHistory latestHistoryRecord, final LocalDate unassignDate) { + if (DateUtils.isBefore(unassignDate, latestHistoryRecord.getStartDate())) { + final String errorMessage = "The Savings officer Unassign date(" + unassignDate + ") cannot be before its assignment date (" + + latestHistoryRecord.getStartDate() + ")."; + throw new SavingsOfficerUnassignmentDateException("cannot.be.before.assignment.date", errorMessage, getId(), + getSavingsOfficer().getId(), latestHistoryRecord.getStartDate(), unassignDate); + } else if (DateUtils.isDateInTheFuture(unassignDate)) { + final String errorMessage = "The Savings Officer Unassign date (" + unassignDate + ") cannot be in the future."; + throw new SavingsOfficerUnassignmentDateException("cannot.be.a.future.date", errorMessage, unassignDate); + } + } + + public MonetaryCurrency getCurrency() { + return this.currency; + } + + public void validateNewApplicationState(final String resourceName) { + // validateWithdrawalFeeDetails(); + // validateAnnualFeeDetails(); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(resourceName + SavingsApiConstants.summitalAction); + + validateLockinDetails(baseDataValidator); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + + final LocalDate submittedOn = getSubmittedOnDate(); + if (DateUtils.isDateInTheFuture(submittedOn)) { + baseDataValidator.reset().parameter(SavingsApiConstants.submittedOnDateParamName).value(submittedOn) + .failWithCodeNoParameterAddedToErrorCode("cannot.be.a.future.date"); + } + + if (this.client != null && this.client.isActivatedAfter(submittedOn)) { + baseDataValidator.reset().parameter(SavingsApiConstants.submittedOnDateParamName).value(this.client.getActivationDate()) + .failWithCodeNoParameterAddedToErrorCode("cannot.be.before.client.activation.date"); + } else if (this.group != null && this.group.isActivatedAfter(submittedOn)) { + baseDataValidator.reset().parameter(SavingsApiConstants.submittedOnDateParamName).value(this.group.getActivationDate()) + .failWithCodeNoParameterAddedToErrorCode("cannot.be.before.client.activation.date"); + } + + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + public Client getClient() { + return this.client; + } + + public BigDecimal getNominalAnnualInterestRate() { + return this.nominalAnnualInterestRate; + } + + public Integer getInterestCompoundingPeriodType() { + return this.interestCompoundingPeriodType; + } + + public Integer getInterestPostingPeriodType() { + return this.interestPostingPeriodType; + } + + public Integer getInterestCalculationType() { + return this.interestCalculationType; + } + + public BigDecimal getNominalAnnualInterestRateOverdraft() { + return this.nominalAnnualInterestRateOverdraft; + } + + public Map approveApplication(final AppUser currentUser, final JsonCommand command) { + final Map actualChanges = new LinkedHashMap<>(); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(SAVINGS_ACCOUNT_RESOURCE_NAME + SavingsApiConstants.approvalAction); + + final SavingsAccountStatusType currentStatus = getStatus(); + if (!SavingsAccountStatusType.SUBMITTED_AND_PENDING_APPROVAL.hasStateOf(currentStatus)) { + baseDataValidator.reset().parameter(SavingsApiConstants.approvedOnDateParamName) + .failWithCodeNoParameterAddedToErrorCode("not.in.submittedandpendingapproval.state"); + + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + this.status = SavingsAccountStatusType.APPROVED.getValue(); + actualChanges.put(SavingsApiConstants.statusParamName, SavingsEnumerations.status(this.status)); + + // only do below if status has changed in the 'approval' case + final LocalDate approvedOn = command.localDateValueOfParameterNamed(SavingsApiConstants.approvedOnDateParamName); + final String approvedOnDateChange = command.stringValueOfParameterNamed(SavingsApiConstants.approvedOnDateParamName); + + this.approvedOnDate = approvedOn; + this.approvedBy = currentUser; + actualChanges.put(SavingsApiConstants.localeParamName, command.locale()); + actualChanges.put(SavingsApiConstants.dateFormatParamName, command.dateFormat()); + actualChanges.put(SavingsApiConstants.approvedOnDateParamName, approvedOnDateChange); + + final LocalDate submittalDate = getSubmittedOnDate(); + if (DateUtils.isBefore(approvedOn, submittalDate)) { + final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(command.extractLocale()); + final String submittalDateAsString = formatter.format(submittalDate); + + baseDataValidator.reset().parameter(SavingsApiConstants.approvedOnDateParamName).value(submittalDateAsString) + .failWithCodeNoParameterAddedToErrorCode("cannot.be.before.submittal.date"); + + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + if (DateUtils.isAfterBusinessDate(approvedOn)) { + baseDataValidator.reset().parameter(SavingsApiConstants.approvedOnDateParamName) + .failWithCodeNoParameterAddedToErrorCode("cannot.be.a.future.date"); + + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + validateActivityNotBeforeClientOrGroupTransferDate(SavingsEvent.SAVINGS_APPLICATION_APPROVED, approvedOn); + + // FIXME - kw - support field officer history for savings accounts + // if (this.fieldOfficer != null) { + // final LoanOfficerAssignmentHistory loanOfficerAssignmentHistory = + // LoanOfficerAssignmentHistory.createNew(this, + // this.fieldOfficer, approvedOn); + // this.loanOfficerHistory.add(loanOfficerAssignmentHistory); + // } + if (this.savingsOfficer != null) { + final SavingsOfficerAssignmentHistory savingsOfficerAssignmentHistory = SavingsOfficerAssignmentHistory.createNew(this, + this.savingsOfficer, approvedOn); + this.savingsOfficerHistory.add(savingsOfficerAssignmentHistory); + } + return actualChanges; + } + + public Map undoApplicationApproval() { + final Map actualChanges = new LinkedHashMap<>(); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(SAVINGS_ACCOUNT_RESOURCE_NAME + SavingsApiConstants.undoApprovalAction); + + final SavingsAccountStatusType currentStatus = getStatus(); + if (!SavingsAccountStatusType.APPROVED.hasStateOf(currentStatus)) { + + baseDataValidator.reset().parameter(SavingsApiConstants.approvedOnDateParamName) + .failWithCodeNoParameterAddedToErrorCode("not.in.approved.state"); + + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + this.status = SavingsAccountStatusType.SUBMITTED_AND_PENDING_APPROVAL.getValue(); + actualChanges.put(SavingsApiConstants.statusParamName, SavingsEnumerations.status(this.status)); + + this.approvedOnDate = null; + this.approvedBy = null; + this.rejectedOnDate = null; + this.rejectedBy = null; + this.withdrawnOnDate = null; + this.withdrawnBy = null; + this.closedOnDate = null; + this.closedBy = null; + actualChanges.put(SavingsApiConstants.approvedOnDateParamName, ""); + + // FIXME - kw - support field officer history for savings accounts + // this.loanOfficerHistory.clear(); + + return actualChanges; + } + + public void undoTransaction(final Long transactionId) { + + SavingsAccountTransaction transactionToUndo = null; + for (final SavingsAccountTransaction transaction : this.transactions) { + if (transaction.isIdentifiedBy(transactionId)) { + transactionToUndo = transaction; + } + } + + if (transactionToUndo == null) { + throw new SavingsAccountTransactionNotFoundException(this.getId(), transactionId); + } + + validateAttemptToUndoTransferRelatedTransactions(transactionToUndo); + validateActivityNotBeforeClientOrGroupTransferDate(SavingsEvent.SAVINGS_UNDO_TRANSACTION, transactionToUndo.getTransactionDate()); + transactionToUndo.reverse(); + if (transactionToUndo.isChargeTransaction() || transactionToUndo.isWaiveCharge()) { + // undo charge + final Set chargesPaidBy = transactionToUndo.getSavingsAccountChargesPaid(); + for (final SavingsAccountChargePaidBy savingsAccountChargePaidBy : chargesPaidBy) { + final SavingsAccountCharge chargeToUndo = savingsAccountChargePaidBy.getSavingsAccountCharge(); + if (transactionToUndo.isChargeTransaction()) { + chargeToUndo.undoPayment(this.getCurrency(), transactionToUndo.getAmount(this.getCurrency())); + } else if (transactionToUndo.isWaiveCharge()) { + chargeToUndo.undoWaiver(this.getCurrency(), transactionToUndo.getAmount(this.getCurrency())); + } + } + } + } + + public void undoSavingsTransaction(final Long transactionId) { + + SavingsAccountTransaction transactionToUndo = null; + for (final SavingsAccountTransaction transaction : this.savingsAccountTransactions) { + if (transaction.isIdentifiedBy(transactionId)) { + transactionToUndo = transaction; + } + } + + if (transactionToUndo == null) { + throw new SavingsAccountTransactionNotFoundException(this.getId(), transactionId); + } + + validateAttemptToUndoTransferRelatedTransactions(transactionToUndo); + validateActivityNotBeforeClientOrGroupTransferDate(SavingsEvent.SAVINGS_UNDO_TRANSACTION, transactionToUndo.getTransactionDate()); + transactionToUndo.reverse(); + if (transactionToUndo.isChargeTransaction() || transactionToUndo.isWaiveCharge()) { + // undo charge + final Set chargesPaidBy = transactionToUndo.getSavingsAccountChargesPaid(); + for (final SavingsAccountChargePaidBy savingsAccountChargePaidBy : chargesPaidBy) { + final SavingsAccountCharge chargeToUndo = savingsAccountChargePaidBy.getSavingsAccountCharge(); + if (transactionToUndo.isChargeTransaction()) { + chargeToUndo.undoPayment(this.getCurrency(), transactionToUndo.getAmount(this.currency)); + } else if (transactionToUndo.isWaiveCharge()) { + chargeToUndo.undoWaiver(this.getCurrency(), transactionToUndo.getAmount(this.currency)); + } + } + } + } + + public void undoTransaction(final SavingsAccountTransaction transactionToUndo) { + + if (transactionToUndo.isReversed()) { + throw new SavingsAccountTransactionNotFoundException(this.getId(), transactionToUndo.getId()); + } + + validateAttemptToUndoTransferRelatedTransactions(transactionToUndo); + validateActivityNotBeforeClientOrGroupTransferDate(SavingsEvent.SAVINGS_UNDO_TRANSACTION, transactionToUndo.getTransactionDate()); + transactionToUndo.reverse(); + if (transactionToUndo.isChargeTransaction() || transactionToUndo.isWaiveCharge()) { + // undo charge + final Set chargesPaidBy = transactionToUndo.getSavingsAccountChargesPaid(); + for (final SavingsAccountChargePaidBy savingsAccountChargePaidBy : chargesPaidBy) { + final SavingsAccountCharge chargeToUndo = savingsAccountChargePaidBy.getSavingsAccountCharge(); + if (transactionToUndo.isChargeTransaction()) { + chargeToUndo.undoPayment(this.getCurrency(), transactionToUndo.getAmount(this.currency)); + } else if (transactionToUndo.isWaiveCharge()) { + chargeToUndo.undoWaiver(this.getCurrency(), transactionToUndo.getAmount(this.currency)); + } + } + } + } + + private LocalDate findLatestAnnualFeeTransactionDueDate() { + LocalDate nextDueDate = null; + + LocalDate lastAnnualFeeTransactionDate = null; + for (final SavingsAccountTransaction transaction : retreiveOrderedNonInterestPostingTransactions()) { + if (transaction.isAnnualFeeAndNotReversed()) { + LocalDate transactionDate = transaction.getTransactionDate(); + if (lastAnnualFeeTransactionDate == null) { + lastAnnualFeeTransactionDate = transactionDate; + nextDueDate = lastAnnualFeeTransactionDate; + } + if (DateUtils.isAfter(transactionDate, lastAnnualFeeTransactionDate)) { + lastAnnualFeeTransactionDate = transactionDate; + nextDueDate = lastAnnualFeeTransactionDate; + } + } + } + return nextDueDate; + } + + public void validateAccountBalanceDoesNotBecomeNegativeMinimal(final BigDecimal transactionAmount, final boolean isException) { + // final List transactionsSortedByDate = retrieveListOfTransactions(); + Money runningBalance = this.summary.getAccountBalance(getCurrency()); + Money minRequiredBalance = minRequiredBalanceDerived(getCurrency()); + final BigDecimal withdrawalFee = null; + + // check last txn date + + // In overdraft cases, minRequiredBalance can be in violation after interest posting + // and should be checked after processing all transactions + if (!isOverdraft()) { + if (runningBalance.minus(minRequiredBalance).isLessThanZero()) { + throw new InsufficientAccountBalanceException("transactionAmount", getAccountBalance(), withdrawalFee, transactionAmount); + } + } + + if (this.getSavingsHoldAmount().compareTo(BigDecimal.ZERO) > 0) { + if (runningBalance.minus(this.getSavingsHoldAmount()).minus(minRequiredBalance).isLessThanZero()) { + throw new InsufficientAccountBalanceException("transactionAmount", getAccountBalance(), withdrawalFee, transactionAmount); + } + } + } + + public Map rejectApplication(final AppUser currentUser, final JsonCommand command) { + final Map actualChanges = new LinkedHashMap<>(); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(SAVINGS_ACCOUNT_RESOURCE_NAME + SavingsApiConstants.rejectAction); + + final SavingsAccountStatusType currentStatus = getStatus(); + if (!SavingsAccountStatusType.SUBMITTED_AND_PENDING_APPROVAL.hasStateOf(currentStatus)) { + + baseDataValidator.reset().parameter(SavingsApiConstants.rejectedOnDateParamName) + .failWithCodeNoParameterAddedToErrorCode("not.in.submittedandpendingapproval.state"); + + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + this.status = SavingsAccountStatusType.REJECTED.getValue(); + actualChanges.put(SavingsApiConstants.statusParamName, SavingsEnumerations.status(this.status)); + + final LocalDate rejectedOn = command.localDateValueOfParameterNamed(SavingsApiConstants.rejectedOnDateParamName); + final String rejectedOnAsString = command.stringValueOfParameterNamed(SavingsApiConstants.rejectedOnDateParamName); + + this.rejectedOnDate = rejectedOn; + this.rejectedBy = currentUser; + this.withdrawnOnDate = null; + this.withdrawnBy = null; + this.closedOnDate = rejectedOn; + this.closedBy = currentUser; + + actualChanges.put(SavingsApiConstants.localeParamName, command.locale()); + actualChanges.put(SavingsApiConstants.dateFormatParamName, command.dateFormat()); + actualChanges.put(SavingsApiConstants.rejectedOnDateParamName, rejectedOnAsString); + actualChanges.put(SavingsApiConstants.closedOnDateParamName, rejectedOnAsString); + + final LocalDate submittalDate = getSubmittedOnDate(); + if (DateUtils.isBefore(rejectedOn, submittalDate)) { + final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(command.extractLocale()); + final String submittalDateAsString = formatter.format(submittalDate); + + baseDataValidator.reset().parameter(SavingsApiConstants.rejectedOnDateParamName).value(submittalDateAsString) + .failWithCodeNoParameterAddedToErrorCode("cannot.be.before.submittal.date"); + + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + if (DateUtils.isAfterBusinessDate(rejectedOn)) { + baseDataValidator.reset().parameter(SavingsApiConstants.rejectedOnDateParamName).value(rejectedOn) + .failWithCodeNoParameterAddedToErrorCode("cannot.be.a.future.date"); + + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + validateActivityNotBeforeClientOrGroupTransferDate(SavingsEvent.SAVINGS_APPLICATION_REJECTED, rejectedOn); + + return actualChanges; + } + + public Map applicantWithdrawsFromApplication(final AppUser currentUser, final JsonCommand command) { + final Map actualChanges = new LinkedHashMap<>(); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(SAVINGS_ACCOUNT_RESOURCE_NAME + SavingsApiConstants.withdrawnByApplicantAction); + + final SavingsAccountStatusType currentStatus = getStatus(); + if (!SavingsAccountStatusType.SUBMITTED_AND_PENDING_APPROVAL.hasStateOf(currentStatus)) { + + baseDataValidator.reset().parameter(SavingsApiConstants.withdrawnOnDateParamName) + .failWithCodeNoParameterAddedToErrorCode("not.in.submittedandpendingapproval.state"); + + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + this.status = SavingsAccountStatusType.WITHDRAWN_BY_APPLICANT.getValue(); + actualChanges.put(SavingsApiConstants.statusParamName, SavingsEnumerations.status(this.status)); + + final LocalDate withdrawnOn = command.localDateValueOfParameterNamed(SavingsApiConstants.withdrawnOnDateParamName); + final String withdrawnOnAsString = command.stringValueOfParameterNamed(SavingsApiConstants.withdrawnOnDateParamName); + + this.rejectedOnDate = null; + this.rejectedBy = null; + this.withdrawnOnDate = withdrawnOn; + this.withdrawnBy = currentUser; + this.closedOnDate = withdrawnOn; + this.closedBy = currentUser; + + actualChanges.put(SavingsApiConstants.localeParamName, command.locale()); + actualChanges.put(SavingsApiConstants.dateFormatParamName, command.dateFormat()); + actualChanges.put(SavingsApiConstants.withdrawnOnDateParamName, withdrawnOnAsString); + actualChanges.put(SavingsApiConstants.closedOnDateParamName, withdrawnOnAsString); + + final LocalDate submittalDate = getSubmittedOnDate(); + if (DateUtils.isBefore(withdrawnOn, submittalDate)) { + final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(command.extractLocale()); + final String submittalDateAsString = formatter.format(submittalDate); + + baseDataValidator.reset().parameter(SavingsApiConstants.withdrawnOnDateParamName).value(submittalDateAsString) + .failWithCodeNoParameterAddedToErrorCode("cannot.be.before.submittal.date"); + + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + if (DateUtils.isAfterBusinessDate(withdrawnOn)) { + baseDataValidator.reset().parameter(SavingsApiConstants.withdrawnOnDateParamName).value(withdrawnOn) + .failWithCodeNoParameterAddedToErrorCode("cannot.be.a.future.date"); + + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + validateActivityNotBeforeClientOrGroupTransferDate(SavingsEvent.SAVINGS_APPLICATION_WITHDRAWAL_BY_CUSTOMER, withdrawnOn); + + return actualChanges; + } + + public Map activate(final AppUser currentUser, final JsonCommand command) { + final Map actualChanges = new LinkedHashMap<>(); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(depositAccountType().resourceName() + SavingsApiConstants.activateAction); + + final SavingsAccountStatusType currentStatus = getStatus(); + if (!SavingsAccountStatusType.APPROVED.hasStateOf(currentStatus)) { + + baseDataValidator.reset().parameter(SavingsApiConstants.activatedOnDateParamName) + .failWithCodeNoParameterAddedToErrorCode("not.in.approved.state"); + + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + final Locale locale = command.extractLocale(); + final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); + final LocalDate activationDate = command.localDateValueOfParameterNamed(SavingsApiConstants.activatedOnDateParamName); + + this.status = SavingsAccountStatusType.ACTIVE.getValue(); + actualChanges.put(SavingsApiConstants.statusParamName, SavingsEnumerations.status(this.status)); + actualChanges.put(SavingsApiConstants.localeParamName, command.locale()); + actualChanges.put(SavingsApiConstants.dateFormatParamName, command.dateFormat()); + actualChanges.put(SavingsApiConstants.activatedOnDateParamName, activationDate.format(fmt)); + + this.rejectedOnDate = null; + this.rejectedBy = null; + this.withdrawnOnDate = null; + this.withdrawnBy = null; + this.closedOnDate = null; + this.closedBy = null; + this.activatedOnDate = activationDate; + this.activatedBy = currentUser; + this.lockedInUntilDate = calculateDateAccountIsLockedUntil(getActivationDate()); + + /* + * if (annualFeeSettingsSet()) { updateToNextAnnualFeeDueDateFrom(getActivationLocalDate()); } + */ + if (this.client != null && this.client.isActivatedAfter(activationDate)) { + final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(command.extractLocale()); + final String dateAsString = formatter.format(this.client.getActivationDate()); + baseDataValidator.reset().parameter(SavingsApiConstants.activatedOnDateParamName).value(dateAsString) + .failWithCodeNoParameterAddedToErrorCode("cannot.be.before.client.activation.date"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + if (this.group != null && this.group.isActivatedAfter(activationDate)) { + final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(command.extractLocale()); + final String dateAsString = formatter.format(this.client.getActivationDate()); + baseDataValidator.reset().parameter(SavingsApiConstants.activatedOnDateParamName).value(dateAsString) + .failWithCodeNoParameterAddedToErrorCode("cannot.be.before.group.activation.date"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + final LocalDate approvalDate = getApprovedOnDate(); + if (DateUtils.isBefore(activationDate, approvalDate)) { + final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(command.extractLocale()); + final String dateAsString = formatter.format(approvalDate); + + baseDataValidator.reset().parameter(SavingsApiConstants.activatedOnDateParamName).value(dateAsString) + .failWithCodeNoParameterAddedToErrorCode("cannot.be.before.approval.date"); + + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + if (DateUtils.isAfterBusinessDate(activationDate)) { + baseDataValidator.reset().parameter(SavingsApiConstants.activatedOnDateParamName).value(activationDate) + .failWithCodeNoParameterAddedToErrorCode("cannot.be.a.future.date"); + + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + validateActivityNotBeforeClientOrGroupTransferDate(SavingsEvent.SAVINGS_ACTIVATE, activationDate); + + return actualChanges; + } + + public void processAccountUponActivation(final boolean isSavingsInterestPostingAtCurrentPeriodEnd, + final Integer financialYearBeginningMonth) { + // update annual fee due date + for (SavingsAccountCharge charge : this.charges()) { + charge.updateToNextDueDateFrom(getActivationDate()); + } + + // auto pay the activation time charges (No need of checking the pivot date config) + this.payActivationCharges(isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, false); + // TODO : AA add activation charges to actual changes list + } + + public Money activateWithBalance() { + return Money.of(this.currency, this.minRequiredOpeningBalance); + } + + public void approveAndActivateApplication(final LocalDate appliedonDate, final AppUser appliedBy) { + this.status = SavingsAccountStatusType.ACTIVE.getValue(); + this.approvedOnDate = appliedonDate; + this.approvedBy = appliedBy; + this.rejectedOnDate = null; + this.rejectedBy = null; + this.withdrawnOnDate = null; + this.withdrawnBy = null; + this.closedOnDate = null; + this.closedBy = null; + this.activatedOnDate = appliedonDate; + this.activatedBy = appliedBy; + this.lockedInUntilDate = calculateDateAccountIsLockedUntil(getActivationDate()); + } + + private void payActivationCharges(final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth, + final boolean backdatedTxnsAllowedTill) { + boolean isSavingsChargeApplied = false; + boolean postReversals = false; + UUID refNo = UUID.randomUUID(); + for (SavingsAccountCharge savingsAccountCharge : this.charges()) { + if (savingsAccountCharge.isSavingsActivation()) { + isSavingsChargeApplied = true; + payCharge(savingsAccountCharge, savingsAccountCharge.getAmountOutstanding(getCurrency()), getActivationDate(), + backdatedTxnsAllowedTill, refNo.toString()); + } + } + + if (isSavingsChargeApplied) { + final MathContext mc = MathContext.DECIMAL64; + boolean isInterestTransfer = false; + LocalDate postInterestAsOnDate = null; + if (this.isBeforeLastPostingPeriod(getActivationDate(), backdatedTxnsAllowedTill)) { + final LocalDate today = DateUtils.getBusinessLocalDate(); + this.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, + postInterestAsOnDate, backdatedTxnsAllowedTill, postReversals); + } else { + final LocalDate today = DateUtils.getBusinessLocalDate(); + this.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth, postInterestAsOnDate, backdatedTxnsAllowedTill, postReversals); + } + } + } + + public Map close(final AppUser currentUser, final JsonCommand command) { + final Map actualChanges = new LinkedHashMap<>(); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(SAVINGS_ACCOUNT_RESOURCE_NAME + SavingsApiConstants.closeAction); + + final SavingsAccountStatusType currentStatus = getStatus(); + 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 (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); + } + } + } + if (getAccountBalance().doubleValue() != 0) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("results.in.balance.not.zero"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + validateActivityNotBeforeClientOrGroupTransferDate(SavingsEvent.SAVINGS_CLOSE_ACCOUNT, closedDate); + this.status = SavingsAccountStatusType.CLOSED.getValue(); + actualChanges.put(SavingsApiConstants.statusParamName, SavingsEnumerations.status(this.status)); + actualChanges.put(SavingsApiConstants.localeParamName, command.locale()); + actualChanges.put(SavingsApiConstants.dateFormatParamName, command.dateFormat()); + actualChanges.put(SavingsApiConstants.closedOnDateParamName, closedDate.format(fmt)); + + this.rejectedOnDate = null; + this.rejectedBy = null; + this.withdrawnOnDate = null; + this.withdrawnBy = null; + this.closedOnDate = closedDate; + this.closedBy = currentUser; + + return actualChanges; + } + + protected void validateActivityNotBeforeClientOrGroupTransferDate(final SavingsEvent event, final LocalDate activityDate) { + if (this.client != null) { + final LocalDate clientOfficeJoiningDate = this.client.getOfficeJoiningDate(); + if (DateUtils.isBefore(activityDate, clientOfficeJoiningDate)) { + throw new SavingsActivityPriorToClientTransferException(event.toString(), clientOfficeJoiningDate); + } + } + } + + private void validateAttemptToUndoTransferRelatedTransactions(final SavingsAccountTransaction savingsAccountTransaction) { + if (savingsAccountTransaction.isTransferRelatedTransaction()) { + throw new SavingsTransferTransactionsCannotBeUndoneException(savingsAccountTransaction.getId()); + } + } + + private LocalDate calculateDateAccountIsLockedUntil(final LocalDate activationLocalDate) { + + LocalDate lockedInUntilLocalDate = null; + final PeriodFrequencyType lockinPeriodFrequencyType = PeriodFrequencyType.fromInt(this.lockinPeriodFrequencyType); + switch (lockinPeriodFrequencyType) { + case INVALID: + break; + case DAYS: + lockedInUntilLocalDate = activationLocalDate.plusDays(this.lockinPeriodFrequency); + break; + case WEEKS: + lockedInUntilLocalDate = activationLocalDate.plusWeeks(this.lockinPeriodFrequency); + break; + case MONTHS: + lockedInUntilLocalDate = activationLocalDate.plusMonths(this.lockinPeriodFrequency); + break; + case YEARS: + lockedInUntilLocalDate = activationLocalDate.plusYears(this.lockinPeriodFrequency); + break; + case WHOLE_TERM: + LOG.error("TODO Implement calculateDateAccountIsLockedUntil for WHOLE_TERM"); + break; + } + + return lockedInUntilLocalDate; + } + + public Group group() { + return this.group; + } + + public boolean isWithdrawalFeeApplicableForTransfer() { + return this.withdrawalFeeApplicableForTransfer; + } + + public void activateAccountBasedOnBalance() { + if (getStatus().isClosed() && !this.summary.getAccountBalance(getCurrency()).isZero()) { + this.status = SavingsAccountStatusType.ACTIVE.getValue(); + } + } + + public LocalDate getClosedOnDate() { + return this.closedOnDate; + } + + public AppUser getClosedBy() { + return this.closedBy; + } + + public SavingsAccountSummary getSummary() { + return this.summary; + } + + public List getTransactions() { + return this.transactions; + } + + public void addTransaction(final SavingsAccountTransaction transaction) { + this.transactions.add(transaction); + } + + public void addTransactionToExisting(final SavingsAccountTransaction transaction) { + this.savingsAccountTransactions.add(transaction); + } + + public void setStatus(final Integer status) { + this.status = status; + } + + private Set associateChargesWithThisSavingsAccount(final Set savingsAccountCharges) { + for (final SavingsAccountCharge savingsAccountCharge : savingsAccountCharges) { + savingsAccountCharge.update(this); + } + return savingsAccountCharges; + } + + public boolean update(final Set newSavingsAccountCharges) { + if (newSavingsAccountCharges == null) { + return false; + } + + if (this.charges == null) { + this.charges = new HashSet<>(); + } + this.charges.clear(); + this.charges.addAll(associateChargesWithThisSavingsAccount(newSavingsAccountCharges)); + return true; + } + + public boolean hasCurrencyCodeOf(final String matchingCurrencyCode) { + if (this.currency == null) { + return false; + } + return this.currency.getCode().equalsIgnoreCase(matchingCurrencyCode); + } + + public void removeCharge(final SavingsAccountCharge charge) { + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(SAVINGS_ACCOUNT_RESOURCE_NAME); + + if (isClosed()) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("delete.transaction.invalid.account.is.closed"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + if (isActive() || isApproved()) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("delete.transaction.invalid.account.is.active"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + this.charges.remove(charge); + } + + public void waiveCharge(final Long savingsAccountChargeId, final boolean backdatedTxnsAllowedTill) { + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(SAVINGS_ACCOUNT_RESOURCE_NAME); + + if (isClosed()) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("transaction.invalid.account.is.closed"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + if (isNotActive()) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("transaction.invalid.account.is.not.active"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + final SavingsAccountCharge savingsAccountCharge = getCharge(savingsAccountChargeId); + + if (savingsAccountCharge.isNotActive()) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("charge.is.not.active"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + if (savingsAccountCharge.isWithdrawalFee()) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("transaction.invalid.waiver.of.withdrawal.fee.not.supported"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + // validate charge is not already paid or waived + if (savingsAccountCharge.isWaived()) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("transaction.invalid.account.charge.is.already.waived"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } else if (savingsAccountCharge.isPaid()) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("transaction.invalid.account.charge.is.paid"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + // waive charge + final Money amountWaived = savingsAccountCharge.waive(getCurrency()); + handleWaiverChargeTransactions(savingsAccountCharge, amountWaived, backdatedTxnsAllowedTill); + } + + public void addCharge(final DateTimeFormatter formatter, final SavingsAccountCharge savingsAccountCharge, + final Charge chargeDefinition) { + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(SAVINGS_ACCOUNT_RESOURCE_NAME); + + if (isClosed()) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("transaction.invalid.account.is.closed"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + if (!hasCurrencyCodeOf(chargeDefinition.getCurrencyCode())) { + baseDataValidator.reset() + .failWithCodeNoParameterAddedToErrorCode("transaction.invalid.account.currency.and.charge.currency.not.same"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + final LocalDate chargeDueDate = savingsAccountCharge.getDueDate(); + + if (savingsAccountCharge.isOnSpecifiedDueDate()) { + if (DateUtils.isBefore(chargeDueDate, getActivationDate())) { + baseDataValidator.reset().parameter(dueAsOfDateParamName).value(getActivationDate().format(formatter)) + .failWithCodeNoParameterAddedToErrorCode("before.activationDate"); + throw new PlatformApiDataValidationException(dataValidationErrors); + } else if (DateUtils.isBefore(chargeDueDate, getSubmittedOnDate())) { + baseDataValidator.reset().parameter(dueAsOfDateParamName).value(getSubmittedOnDate().format(formatter)) + .failWithCodeNoParameterAddedToErrorCode("before.submittedOnDate"); + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + if (savingsAccountCharge.isSavingsActivation() && !(isSubmittedAndPendingApproval() || (isApproved() && isNotActive()))) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("not.valid.account.status.cannot.add.activation.time.charge"); + throw new PlatformApiDataValidationException(dataValidationErrors); + } + + // Only one annual fee is supported per account + if (savingsAccountCharge.isAnnualFee()) { + if (this.isAnnualFeeExists()) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("multiple.annual.fee.per.account.not.supported"); + throw new PlatformApiDataValidationException(dataValidationErrors); + } + + } + + if (savingsAccountCharge.isAnnualFee() || savingsAccountCharge.isMonthlyFee() || savingsAccountCharge.isWeeklyFee()) { + // update due date + if (isActive()) { + savingsAccountCharge.updateToNextDueDateFrom(getActivationDate()); + } else if (isApproved()) { + savingsAccountCharge.updateToNextDueDateFrom(getApprovedOnDate()); + } + } + + // activation charge and withdrawal charges not required this validation + if (savingsAccountCharge.isOnSpecifiedDueDate()) { + validateActivityNotBeforeClientOrGroupTransferDate(SavingsEvent.SAVINGS_APPLY_CHARGE, chargeDueDate); + } + + // add new charge to savings account + this.charges.add(savingsAccountCharge); + + } + + private boolean isAnnualFeeExists() { + for (SavingsAccountCharge charge : this.charges()) { + if (charge.isAnnualFee()) { + return true; + } + } + return false; + } + + public SavingsAccountTransaction payCharge(final SavingsAccountCharge savingsAccountCharge, final BigDecimal amountPaid, + final LocalDate transactionDate, final DateTimeFormatter formatter, final boolean backdatedTxnsAllowedTill, + final String refNo) { + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(SAVINGS_ACCOUNT_RESOURCE_NAME); + + if (isClosed()) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("transaction.invalid.account.is.closed"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + if (isNotActive()) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("transaction.invalid.account.is.not.active"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + if (savingsAccountCharge.isNotActive()) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("charge.is.not.active"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + if (DateUtils.isBefore(transactionDate, getActivationDate())) { + baseDataValidator.reset().parameter(dueAsOfDateParamName).value(getActivationDate().format(formatter)) + .failWithCodeNoParameterAddedToErrorCode("transaction.before.activationDate"); + throw new PlatformApiDataValidationException(dataValidationErrors); + } + + if (DateUtils.isDateInTheFuture(transactionDate)) { + baseDataValidator.reset().parameter(dueAsOfDateParamName).value(transactionDate.format(formatter)) + .failWithCodeNoParameterAddedToErrorCode("transaction.is.futureDate"); + throw new PlatformApiDataValidationException(dataValidationErrors); + } + + if (savingsAccountCharge.isSavingsActivation()) { + baseDataValidator.reset() + .failWithCodeNoParameterAddedToErrorCode("transaction.not.valid.cannot.pay.activation.time.charge.is.automated"); + throw new PlatformApiDataValidationException(dataValidationErrors); + } + + if (savingsAccountCharge.isAnnualFee()) { + final LocalDate annualFeeDueDate = savingsAccountCharge.getDueDate(); + if (annualFeeDueDate == null) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("no.annualfee.settings"); + throw new PlatformApiDataValidationException(dataValidationErrors); + } + + if (DateUtils.isBefore(transactionDate, annualFeeDueDate)) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("transaction.before.dueDate"); + throw new PlatformApiDataValidationException(dataValidationErrors); + } + + LocalDate currentAnnualFeeNextDueDate = findLatestAnnualFeeTransactionDueDate(); + if (DateUtils.isEqual(currentAnnualFeeNextDueDate, transactionDate)) { + baseDataValidator.reset().parameter("dueDate").value(transactionDate.format(formatter)) + .failWithCodeNoParameterAddedToErrorCode("transaction.exists.on.date"); + + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + // validate charge is not already paid or waived + if (savingsAccountCharge.isWaived()) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("transaction.invalid.account.charge.is.already.waived"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } else if (savingsAccountCharge.isPaid()) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("transaction.invalid.account.charge.is.paid"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + final Money chargePaid = Money.of(currency, amountPaid); + if (!savingsAccountCharge.getAmountOutstanding(getCurrency()).isGreaterThanOrEqualTo(chargePaid)) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("transaction.invalid.charge.amount.paid.in.access"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + return this.payCharge(savingsAccountCharge, chargePaid, transactionDate, backdatedTxnsAllowedTill, refNo); + } + + public SavingsAccountTransaction payCharge(final SavingsAccountCharge savingsAccountCharge, final Money amountPaid, + final LocalDate transactionDate, final boolean backdatedTxnsAllowedTill, String refNo) { + savingsAccountCharge.pay(getCurrency(), amountPaid); + return handlePayChargeTransactions(savingsAccountCharge, amountPaid, transactionDate, backdatedTxnsAllowedTill, refNo); + } + + private SavingsAccountTransaction handlePayChargeTransactions(SavingsAccountCharge savingsAccountCharge, Money transactionAmount, + final LocalDate transactionDate, final boolean backdatedTxnsAllowedTill, final String refNo) { + SavingsAccountTransaction chargeTransaction; + + if (savingsAccountCharge.isWithdrawalFee()) { + chargeTransaction = SavingsAccountTransaction.withdrawalFee(this, office(), transactionDate, transactionAmount, refNo); + } else if (savingsAccountCharge.isAnnualFee()) { + chargeTransaction = SavingsAccountTransaction.annualFee(this, office(), transactionDate, transactionAmount); + } else { + chargeTransaction = SavingsAccountTransaction.charge(this, office(), transactionDate, transactionAmount); + } + + handleChargeTransactions(savingsAccountCharge, chargeTransaction, backdatedTxnsAllowedTill); + return chargeTransaction; + } + + private void handleWaiverChargeTransactions(SavingsAccountCharge savingsAccountCharge, Money transactionAmount, + boolean backdatedTxnsAllowedTill) { + final SavingsAccountTransaction chargeTransaction = SavingsAccountTransaction.waiver(this, office(), + DateUtils.getBusinessLocalDate(), transactionAmount); + handleChargeTransactions(savingsAccountCharge, chargeTransaction, backdatedTxnsAllowedTill); + } + + private void handleChargeTransactions(final SavingsAccountCharge savingsAccountCharge, final SavingsAccountTransaction transaction, + final boolean backdatedTxnsAllowedTill) { + // Provide a link between transaction and savings charge for which + // amount is waived. + final SavingsAccountChargePaidBy chargePaidBy = SavingsAccountChargePaidBy.instance(transaction, savingsAccountCharge, + transaction.getAmount(this.currency).getAmount()); + transaction.getSavingsAccountChargesPaid().add(chargePaidBy); + if (backdatedTxnsAllowedTill) { + this.savingsAccountTransactions.add(transaction); + this.summary.updateSummaryWithPivotConfig(this.currency, this.savingsAccountTransactionSummaryWrapper, transaction, + this.savingsAccountTransactions); + } else { + this.transactions.add(transaction); + } + } + + private SavingsAccountCharge getCharge(final Long savingsAccountChargeId) { + SavingsAccountCharge charge = null; + for (final SavingsAccountCharge existingCharge : this.charges) { + if (existingCharge.getId().equals(savingsAccountChargeId)) { + charge = existingCharge; + break; + } + } + + if (charge == null) { + throw new SavingsAccountChargeNotFoundException(savingsAccountChargeId, getId()); + } + + return charge; + } + + public Set charges() { + return (this.charges == null) ? new HashSet() : this.charges; + } + + public void validateAccountValuesWithProduct() { + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(SAVINGS_ACCOUNT_RESOURCE_NAME); + + if (this.overdraftLimit != null && this.product.overdraftLimit() != null + && this.overdraftLimit.compareTo(this.product.overdraftLimit()) > 0) { + baseDataValidator.reset().parameter(SavingsApiConstants.overdraftLimitParamName).value(this.overdraftLimit) + .failWithCode("cannot.exceed.product.value"); + } + + validateInterestPostingAndCompoundingPeriodTypes(baseDataValidator); + + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + public boolean allowOverdraft() { + return this.allowOverdraft; + } + + public LocalDate accountSubmittedOrActivationDate() { + return getActivationDate() == null ? getSubmittedOnDate() : getActivationDate(); + } + + protected boolean isTransferInterestToOtherAccount() { + return false; + } + + public boolean accountSubmittedAndActivationOnSameDate() { + return getActivationDate() != null && DateUtils.isEqual(getActivationDate(), getSubmittedOnDate()); + + } + + public void validateInterestPostingAndCompoundingPeriodTypes(final DataValidatorBuilder baseDataValidator) { + Map> postingtoCompoundMap = new HashMap<>(); + + postingtoCompoundMap.put(SavingsPostingInterestPeriodType.DAILY, Arrays.asList(SavingsCompoundingInterestPeriodType.DAILY)); + + postingtoCompoundMap.put(SavingsPostingInterestPeriodType.MONTHLY, + Arrays.asList(SavingsCompoundingInterestPeriodType.DAILY, SavingsCompoundingInterestPeriodType.MONTHLY)); + + postingtoCompoundMap.put(SavingsPostingInterestPeriodType.QUATERLY, Arrays.asList(SavingsCompoundingInterestPeriodType.DAILY, + SavingsCompoundingInterestPeriodType.MONTHLY, SavingsCompoundingInterestPeriodType.QUATERLY)); + + postingtoCompoundMap.put(SavingsPostingInterestPeriodType.BIANNUAL, + Arrays.asList(SavingsCompoundingInterestPeriodType.DAILY, SavingsCompoundingInterestPeriodType.MONTHLY, + SavingsCompoundingInterestPeriodType.QUATERLY, SavingsCompoundingInterestPeriodType.BI_ANNUAL)); + + postingtoCompoundMap.put(SavingsPostingInterestPeriodType.ANNUAL, + Arrays.asList(SavingsCompoundingInterestPeriodType.DAILY, SavingsCompoundingInterestPeriodType.MONTHLY, + SavingsCompoundingInterestPeriodType.QUATERLY, SavingsCompoundingInterestPeriodType.BI_ANNUAL, + SavingsCompoundingInterestPeriodType.ANNUAL)); + + SavingsPostingInterestPeriodType savingsPostingInterestPeriodType = SavingsPostingInterestPeriodType + .fromInt(interestPostingPeriodType); + SavingsCompoundingInterestPeriodType savingsCompoundingInterestPeriodType = SavingsCompoundingInterestPeriodType + .fromInt(interestCompoundingPeriodType); + + if (postingtoCompoundMap.get(savingsPostingInterestPeriodType) == null) { + baseDataValidator.failWithCodeNoParameterAddedToErrorCode("posting.period.type.is.less.than.compound.period.type", + savingsPostingInterestPeriodType.name(), savingsCompoundingInterestPeriodType.name()); + + } + } + + public AppUser getSubmittedBy() { + return this.submittedBy; + } + + public AppUser getApprovedBy() { + return this.approvedBy; + } + + public boolean allowDeposit() { + return true; + } + + public boolean allowWithdrawal() { + return true; + } + + public boolean allowModify() { + return true; + } + + public boolean isTransactionsAllowed() { + return isActive(); + } + + public boolean isTransactionAllowed(SavingsAccountTransactionType transactionType, LocalDate transactionDate) { + if (!isTransactionsAllowed()) { + return false; + } + + Client client = getClient(); + if (client != null && !client.isActive()) { + return false; + } + Group group = group(); + if (group != null && !group.isActive()) { + return false; + } + + if (transactionDate == null) { + return true; + } + if (DateUtils.isDateInTheFuture(transactionDate) || DateUtils.isBefore(transactionDate, getActivationDate())) { + return false; + } + if (transactionType.isCredit()) { + return true; + } + return !isAccountLocked(transactionDate); + } + + public BigDecimal minBalanceForInterestCalculation() { + return this.minBalanceForInterestCalculation; + } + + public void inactivateCharge(SavingsAccountCharge savingsAccountCharge, LocalDate inactivationOnDate) { + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(SAVINGS_ACCOUNT_RESOURCE_NAME); + + if (isClosed()) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("transaction.invalid.account.is.closed"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + if (isNotActive()) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("transaction.invalid.account.is.not.active"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + savingsAccountCharge.inactiavateCharge(inactivationOnDate); + } + + public SavingsAccountCharge getUpdatedChargeDetails(SavingsAccountCharge savingsAccountCharge) { + for (final SavingsAccountCharge charge : this.charges) { + if (charge.equals(savingsAccountCharge)) { + savingsAccountCharge = charge; + break; + } + } + return savingsAccountCharge; + } + + public String getAccountNumber() { + return this.accountNumber; + } + + private Money minRequiredBalanceDerived(final MonetaryCurrency currency) { + Money minReqBalance = Money.zero(currency); + if (this.enforceMinRequiredBalance) { + minReqBalance = minReqBalance.plus(this.minRequiredBalance); + } + if (this.allowOverdraft) { + minReqBalance = minReqBalance.minus(this.overdraftLimit); + } + return minReqBalance; + } + + public BigDecimal getOnHoldFunds() { + return this.onHoldFunds == null ? BigDecimal.ZERO : this.onHoldFunds; + } + + public void holdFunds(BigDecimal onHoldFunds) { + this.onHoldFunds = getOnHoldFunds().add(onHoldFunds); + } + + public void releaseFunds(BigDecimal onHoldFunds) { + this.onHoldFunds = getOnHoldFunds().subtract(onHoldFunds); + } + + public BigDecimal getWithdrawableBalance() { + return getAccountBalance().subtract(minRequiredBalanceDerived(getCurrency()).getAmount()).subtract(this.getOnHoldFunds()) + .subtract(this.getSavingsHoldAmount()); + } + + public BigDecimal getWithdrawableBalanceWithLien() { + return getAccountBalance().subtract(minRequiredBalanceDerived(getCurrency()).getAmount()).subtract(this.getOnHoldFunds()) + .subtract(this.getSavingsHoldAmount()); + } + + public BigDecimal getWithdrawableBalanceWithoutMinimumBalance() { + return getAccountBalance().subtract(this.getOnHoldFunds()).subtract(this.getSavingsHoldAmount()); + } + + public TaxGroup getTaxGroup() { + return this.taxGroup; + } + + public boolean withHoldTax() { + return this.withHoldTax; + } + + public void setWithHoldTax(boolean withHoldTax) { + this.withHoldTax = withHoldTax; + } + + protected boolean applyWithholdTaxForDepositAccounts(final LocalDate interestPostingUpToDate, boolean recalucateDailyBalance, + final boolean backdatedTxnsAllowedTill) { + final List withholdTransactions = findWithHoldTransactions(); + SavingsAccountTransaction withholdTransaction = findTransactionFor(interestPostingUpToDate, withholdTransactions); + final BigDecimal totalInterestPosted = this.savingsAccountTransactionSummaryWrapper.calculateTotalInterestPosted(this.currency, + this.transactions); + if (withholdTransaction == null && this.withHoldTax()) { + boolean isWithholdTaxAdded = createWithHoldTransaction(totalInterestPosted, interestPostingUpToDate, backdatedTxnsAllowedTill); + recalucateDailyBalance = recalucateDailyBalance || isWithholdTaxAdded; + } else { + boolean isWithholdTaxAdded = updateWithHoldTransaction(totalInterestPosted, withholdTransaction); + recalucateDailyBalance = recalucateDailyBalance || isWithholdTaxAdded; + } + + return recalucateDailyBalance; + } + + public void setSubStatusInactive(final boolean backdatedTxnsAllowedTill) { + this.sub_status = SavingsAccountSubStatusEnum.INACTIVE.getValue(); + LocalDate transactionDate = DateUtils.getBusinessLocalDate(); + for (SavingsAccountCharge charge : this.charges()) { + if (charge.isSavingsNoActivity() && charge.isActive()) { + charge.updateWithdralFeeAmount(this.getAccountBalance()); + UUID refNo = UUID.randomUUID(); + this.payCharge(charge, charge.getAmountOutstanding(this.getCurrency()), transactionDate, backdatedTxnsAllowedTill, + refNo.toString()); + } + } + boolean postReversals = false; + recalculateDailyBalances(Money.zero(this.currency), transactionDate, backdatedTxnsAllowedTill, postReversals); + this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, this.transactions); + } + + public void recalculateBalancesAfterFacilityCharges(final LocalDate transactionDate) { + recalculateDailyBalances(Money.zero(this.currency), transactionDate, false, false); + this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, this.transactions); + } + + public void setSubStatusDormant() { + this.sub_status = SavingsAccountSubStatusEnum.DORMANT.getValue(); + } + + public void escheat(AppUser appUser) { + this.status = SavingsAccountStatusType.CLOSED.getValue(); + this.sub_status = SavingsAccountSubStatusEnum.ESCHEAT.getValue(); + this.closedOnDate = DateUtils.getBusinessLocalDate(); + this.closedBy = appUser; + boolean postInterestAsOnDate = false; + boolean postReversals = false; + LocalDate transactionDate = DateUtils.getBusinessLocalDate(); + if (this.getSummary().getAccountBalance(this.getCurrency()).isGreaterThanZero()) { + SavingsAccountTransaction transaction = SavingsAccountTransaction.escheat(this, transactionDate, postInterestAsOnDate); + this.transactions.add(transaction); + } + recalculateDailyBalances(Money.zero(this.currency), transactionDate, false, postReversals); + this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, this.transactions); + } + + public void loadLazyCollections() { + transactions.size(); + charges.size(); + savingsOfficerHistory.size(); + if (group != null) { + Office dummyOffice = group.getOffice(); + } // Ensure lazy loading of group if set + } + + public void updateSavingsAccountSummary(final List transactions) { + this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, transactions); + } + + public void updateReason(final String reasonForBlock) { + this.reasonForBlock = reasonForBlock; + } + + public Map block() { + + final Map actualChanges = new LinkedHashMap<>(); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(depositAccountType().resourceName() + SavingsApiConstants.blockAction); + + final SavingsAccountStatusType currentStatus = getStatus(); + if (!SavingsAccountStatusType.ACTIVE.hasStateOf(currentStatus)) { + + baseDataValidator.reset().parameter(SavingsApiConstants.statusParamName) + .failWithCodeNoParameterAddedToErrorCode(SavingsApiConstants.ERROR_MSG_SAVINGS_ACCOUNT_NOT_ACTIVE); + + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + this.sub_status = SavingsAccountSubStatusEnum.BLOCK.getValue(); + actualChanges.put(SavingsApiConstants.subStatusParamName, SavingsEnumerations.subStatus(this.sub_status)); + + return actualChanges; + } + + public Map unblock() { + + final Map actualChanges = new LinkedHashMap<>(); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(depositAccountType().resourceName() + SavingsApiConstants.unblockAction); + + final SavingsAccountStatusType currentStatus = getStatus(); + if (!SavingsAccountStatusType.ACTIVE.hasStateOf(currentStatus)) { + + baseDataValidator.reset().parameter(SavingsApiConstants.statusParamName) + .failWithCodeNoParameterAddedToErrorCode(SavingsApiConstants.ERROR_MSG_SAVINGS_ACCOUNT_NOT_ACTIVE); + + } + + final SavingsAccountSubStatusEnum currentSubStatus = SavingsAccountSubStatusEnum.fromInt(this.sub_status); + if (!SavingsAccountSubStatusEnum.BLOCK.hasStateOf(currentSubStatus)) { + baseDataValidator.reset().parameter(SavingsApiConstants.subStatusParamName) + .failWithCodeNoParameterAddedToErrorCode("not.in.blocked.state"); + } + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + this.sub_status = SavingsAccountSubStatusEnum.NONE.getValue(); + actualChanges.put(SavingsApiConstants.subStatusParamName, SavingsEnumerations.subStatus(this.sub_status)); + return actualChanges; + } + + public Map blockCredits(Integer currentSubstatus) { + + final Map actualChanges = new LinkedHashMap<>(); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(depositAccountType().resourceName() + SavingsApiConstants.blockCreditsAction); + + final SavingsAccountStatusType currentStatus = getStatus(); + if (!SavingsAccountStatusType.ACTIVE.hasStateOf(currentStatus)) { + + baseDataValidator.reset().parameter(SavingsApiConstants.statusParamName) + .failWithCodeNoParameterAddedToErrorCode(SavingsApiConstants.ERROR_MSG_SAVINGS_ACCOUNT_NOT_ACTIVE); + } + if (SavingsAccountSubStatusEnum.BLOCK.hasStateOf(SavingsAccountSubStatusEnum.fromInt(currentSubstatus))) { + + baseDataValidator.reset().parameter(SavingsApiConstants.subStatusParamName) + .value(SavingsAccountSubStatusEnum.fromInt(currentSubstatus)).failWithCodeNoParameterAddedToErrorCode("currently.set"); + } + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + if (SavingsAccountSubStatusEnum.BLOCK_DEBIT.hasStateOf(SavingsAccountSubStatusEnum.fromInt(currentSubstatus))) { + this.sub_status = SavingsAccountSubStatusEnum.BLOCK.getValue(); + } else { + this.sub_status = SavingsAccountSubStatusEnum.BLOCK_CREDIT.getValue(); + } + actualChanges.put(SavingsApiConstants.subStatusParamName, SavingsEnumerations.subStatus(this.sub_status)); + + return actualChanges; + } + + public Map unblockCredits() { + + final Map actualChanges = new LinkedHashMap<>(); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(depositAccountType().resourceName() + SavingsApiConstants.unblockCreditsAction); + + final SavingsAccountStatusType currentStatus = getStatus(); + if (!SavingsAccountStatusType.ACTIVE.hasStateOf(currentStatus)) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode(SavingsApiConstants.ERROR_MSG_SAVINGS_ACCOUNT_NOT_ACTIVE); + } + + final SavingsAccountSubStatusEnum currentSubStatus = SavingsAccountSubStatusEnum.fromInt(this.sub_status); + if (!(SavingsAccountSubStatusEnum.BLOCK_CREDIT.hasStateOf(currentSubStatus) + || SavingsAccountSubStatusEnum.BLOCK.hasStateOf(currentSubStatus))) { + baseDataValidator.reset().parameter(SavingsApiConstants.statusParamName) + .failWithCodeNoParameterAddedToErrorCode("credits.are.not.blocked"); + } + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + if (SavingsAccountSubStatusEnum.BLOCK.hasStateOf(currentSubStatus)) { + this.sub_status = SavingsAccountSubStatusEnum.BLOCK_DEBIT.getValue(); + } else { + this.sub_status = SavingsAccountSubStatusEnum.NONE.getValue(); + } + actualChanges.put(SavingsApiConstants.subStatusParamName, SavingsEnumerations.subStatus(this.sub_status)); + return actualChanges; + } + + public Map blockDebits(Integer currentSubstatus) { + + final Map actualChanges = new LinkedHashMap<>(); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(depositAccountType().resourceName() + SavingsApiConstants.blockDebitsAction); + + final SavingsAccountStatusType currentStatus = getStatus(); + if (!SavingsAccountStatusType.ACTIVE.hasStateOf(currentStatus)) { + baseDataValidator.reset().parameter(SavingsApiConstants.statusParamName) + .failWithCodeNoParameterAddedToErrorCode(SavingsApiConstants.ERROR_MSG_SAVINGS_ACCOUNT_NOT_ACTIVE); + + } + if (SavingsAccountSubStatusEnum.BLOCK.hasStateOf(SavingsAccountSubStatusEnum.fromInt(currentSubstatus))) { + + baseDataValidator.reset().parameter(SavingsApiConstants.subStatusParamName) + .value(SavingsAccountSubStatusEnum.fromInt(currentSubstatus)).failWithCodeNoParameterAddedToErrorCode("currently.set"); + } + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + if (SavingsAccountSubStatusEnum.BLOCK_CREDIT.hasStateOf(SavingsAccountSubStatusEnum.fromInt(currentSubstatus))) { + this.sub_status = SavingsAccountSubStatusEnum.BLOCK.getValue(); + } else { + this.sub_status = SavingsAccountSubStatusEnum.BLOCK_DEBIT.getValue(); + } + actualChanges.put(SavingsApiConstants.subStatusParamName, SavingsEnumerations.subStatus(this.sub_status)); + + return actualChanges; + } + + public Map unblockDebits() { + + final Map actualChanges = new LinkedHashMap<>(); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(depositAccountType().resourceName() + SavingsApiConstants.unblockDebitsAction); + + final SavingsAccountStatusType currentStatus = getStatus(); + if (!SavingsAccountStatusType.ACTIVE.hasStateOf(currentStatus)) { + + baseDataValidator.reset().parameter(SavingsApiConstants.statusParamName) + .failWithCodeNoParameterAddedToErrorCode(SavingsApiConstants.ERROR_MSG_SAVINGS_ACCOUNT_NOT_ACTIVE); + + } + + final SavingsAccountSubStatusEnum currentSubStatus = SavingsAccountSubStatusEnum.fromInt(this.sub_status); + if (!(SavingsAccountSubStatusEnum.BLOCK_DEBIT.hasStateOf(currentSubStatus) + || SavingsAccountSubStatusEnum.BLOCK.hasStateOf(currentSubStatus))) { + baseDataValidator.reset().parameter(SavingsApiConstants.subStatusParamName) + .failWithCodeNoParameterAddedToErrorCode("debits.are.not.blocked"); + } + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + if (SavingsAccountSubStatusEnum.BLOCK.hasStateOf(currentSubStatus)) { + this.sub_status = SavingsAccountSubStatusEnum.BLOCK_CREDIT.getValue(); + } else { + this.sub_status = SavingsAccountSubStatusEnum.NONE.getValue(); + } + actualChanges.put(SavingsApiConstants.subStatusParamName, SavingsEnumerations.subStatus(this.sub_status)); + return actualChanges; + } + + public SavingsAccountStatusType getStatus() { + return SavingsAccountStatusType.fromInt(status); + } + + public Integer getSubStatus() { + return this.sub_status; + } + + public void validateForAccountBlock() { + final SavingsAccountSubStatusEnum currentSubStatus = SavingsAccountSubStatusEnum.fromInt(this.getSubStatus()); + if (SavingsAccountSubStatusEnum.BLOCK.hasStateOf(currentSubStatus)) { + throw new SavingsAccountBlockedException(this.getId()); + } + } + + public void validateForDebitBlock() { + final SavingsAccountSubStatusEnum currentSubStatus = SavingsAccountSubStatusEnum.fromInt(this.getSubStatus()); + if (SavingsAccountSubStatusEnum.BLOCK_DEBIT.hasStateOf(currentSubStatus)) { + throw new SavingsAccountDebitsBlockedException(this.getId()); + } + } + + public void validateForCreditBlock() { + final SavingsAccountSubStatusEnum currentSubStatus = SavingsAccountSubStatusEnum.fromInt(this.getSubStatus()); + if (SavingsAccountSubStatusEnum.BLOCK_CREDIT.hasStateOf(currentSubStatus)) { + throw new SavingsAccountCreditsBlockedException(this.getId()); + } + } + + public LocalDate retrieveLastTransactionDate() { + final List transactionsSortedByDate = retrieveListOfTransactions(); + SavingsAccountTransaction lastTransaction = null; + if (transactionsSortedByDate.size() > 0) { + lastTransaction = transactionsSortedByDate.get(transactionsSortedByDate.size() - 1); + } + LocalDate lastransactionDate = null; + if (lastTransaction != null) { + lastransactionDate = lastTransaction.getTransactionDate(); + } + return lastransactionDate; + } + + public LocalDate retrieveLastTransactionDateWithPivotConfig() { + final List transactionsSortedByDate = retrieveSortedTransactions(); + SavingsAccountTransaction lastTransaction = null; + if (transactionsSortedByDate.size() > 0) { + lastTransaction = transactionsSortedByDate.get(transactionsSortedByDate.size() - 1); + } + LocalDate lastransactionDate = null; + if (lastTransaction != null) { + lastransactionDate = lastTransaction.getTransactionDate(); + } + return lastransactionDate; + } + + public BigDecimal getSavingsHoldAmount() { + return this.savingsOnHoldAmount == null ? BigDecimal.ZERO : this.savingsOnHoldAmount; + } + + public void holdAmount(BigDecimal amount) { + this.savingsOnHoldAmount = getSavingsHoldAmount().add(amount); + } + + public void releaseOnHoldAmount(BigDecimal amount) { + this.savingsOnHoldAmount = getSavingsHoldAmount().subtract(amount); + } + + public AccountType getAccountType() { + return AccountType.fromInt(accountType); + } + + public Integer getAccountTypes() { + return accountType; + } + + public void setAccountType(Integer accountType) { + this.accountType = accountType; + } + + private boolean isOverdraft() { + return allowOverdraft; + } + + public Long getGroupId() { + return this.groupId(); + } + + public Integer getInterestCalculationDaysInYearType() { + return this.interestCalculationDaysInYearType; + } + + public BigDecimal getMinRequiredOpeningBalance() { + return this.minRequiredOpeningBalance; + } + + public Integer getLockinPeriodFrequency() { + return this.lockinPeriodFrequency; + } + + public Integer getLockinPeriodFrequencyType() { + return this.lockinPeriodFrequencyType; + } + + public boolean isWithdrawalFeeForTransfer() { + return this.withdrawalFeeApplicableForTransfer; + } + + public boolean isAllowOverdraft() { + return this.allowOverdraft; + } + + public BigDecimal getOverdraftLimit() { + return this.overdraftLimit; + } + + public BigDecimal getMinOverdraftForInterestCalculation() { + return this.minOverdraftForInterestCalculation; + } + + public BigDecimal getMinRequiredBalance() { + return this.minRequiredBalance; + } + + public boolean isEnforceMinRequiredBalance() { + return this.enforceMinRequiredBalance; + } + + public BigDecimal getMaxAllowedLienLimit() { + return this.maxAllowedLienLimit; + } + + public boolean isLienAllowed() { + return this.lienAllowed; + } + + public BigDecimal getMinBalanceForInterestCalculation() { + return this.minBalanceForInterestCalculation; + } + + public int getVersion() { + return this.version; + } + + public boolean isWithHoldTax() { + return this.withHoldTax; + } + + public void setAccruedTillDate(LocalDate accruedTillDate) { + this.accruedTillDate = accruedTillDate; + } + + public LocalDate getLastClosedBusinessDate() { + return this.lastClosedBusinessDate; + } + + public void setLastClosedBusinessDate(LocalDate lastClosedBusinessDate) { + this.lastClosedBusinessDate = lastClosedBusinessDate; + } + + public List toSavingsAccountTransactionDetailsForPostingPeriodList( + List transactions) { + return transactions.stream() + .map(transaction -> transaction.toSavingsAccountTransactionDetailsForPostingPeriod(this.currency, this.allowOverdraft)) + .toList(); + } + + public void accrualsForSavingsReverse(SavingsAccountTransactionDTO transactionDTO, final boolean backdatedTxnsAllowedTill) { + List accountTransactionsSorted = null; + + if (backdatedTxnsAllowedTill) { + accountTransactionsSorted = retrieveSortedTransactions(); + } else { + accountTransactionsSorted = retrieveListOfTransactions(); + } + for (final SavingsAccountTransaction transaction : accountTransactionsSorted) { + boolean typeTransaccionValidation = transaction.getTransactionType() == SavingsAccountTransactionType.ACCRUAL; + if (typeTransaccionValidation && (transaction.getDateOf().isAfter(transactionDTO.getTransactionDate()) + || transaction.getDateOf().isEqual(transactionDTO.getTransactionDate()))) { + transaction.reverse(); + } + } + } + + public List toSavingsAccountTransactionDetailsForPostingPeriodList() { + return retreiveOrderedNonInterestPostingTransactions().stream() + .map(transaction -> transaction.toSavingsAccountTransactionDetailsForPostingPeriod(this.currency, this.allowOverdraft)) + .toList(); + } + + @Override + public DepositAccountType depositAccountType() { + return DepositAccountType.fromInt(100); + } + + // ─── CBS extended fields ─────────────────────────────────────────────────── + + @Column(name = "cbs_account_type", length = 100) + private String cbsAccountType; + + @Column(name = "customer_segment", length = 100) + private String customerSegment; + + @Column(name = "account_status", length = 50) + private String accountStatus; + + @Column(name = "mode_of_operation", length = 50) + private String modeOfOperation; + + @Column(name = "special_category", length = 100) + private String specialCategory; + + @Column(name = "weaker_section", length = 100) + private String weakerSection; + + @Column(name = "guardian_name", length = 200) + private String guardianName; + + @Column(name = "guardian_cif_number", length = 50) + private String guardianCifNumber; + + @Column(name = "guardian_relationship", length = 100) + private String guardianRelationship; + + @Column(name = "minor_date_of_birth") + private LocalDate minorDateOfBirth; + + @Column(name = "guardian_client_id") + private Long guardianClientId; + + @Column(name = "introducer_client_id") + private Long introducerClientId; + + @Column(name = "introducer_name", length = 200) + private String introducerName; + + @Column(name = "introducer_cif_number", length = 50) + private String introducerCifNumber; + + @Column(name = "nominee_data", columnDefinition = "TEXT") + private String nomineeData; + + @Column(name = "linked_account_id") + private Long linkedAccountId; + + @Column(name = "linked_account_type", length = 20) + private String linkedAccountType; + + @Column(name = "linked_account_no", length = 50) + private String linkedAccountNo; + + @Column(name = "nomination_facility", nullable = false, columnDefinition = "boolean default false") + private Boolean nominationFacility = false; + + @Column(name = "auto_debit_facility", nullable = false, columnDefinition = "boolean default false") + private Boolean autoDebitFacility = false; + + + @jakarta.persistence.OneToOne(cascade = jakarta.persistence.CascadeType.ALL, mappedBy = "savingsAccount", orphanRemoval = true, fetch = FetchType.LAZY) + private SavingsAccountFacilityConfig facilityConfig; + + @OneToMany(cascade = CascadeType.ALL, mappedBy = "savingsAccount", orphanRemoval = true, fetch = FetchType.LAZY) + private List jointHolders = new ArrayList<>(); + + public String getCbsAccountType() { return cbsAccountType; } + public void setCbsAccountType(String v) { this.cbsAccountType = v; } + + public String getCustomerSegment() { return customerSegment; } + public void setCustomerSegment(String v) { this.customerSegment = v; } + + public String getAccountStatus() { return accountStatus; } + public void setAccountStatus(String v) { this.accountStatus = v; } + + public String getModeOfOperation() { return modeOfOperation; } + public void setModeOfOperation(String v) { this.modeOfOperation = v; } + + public String getSpecialCategory() { return specialCategory; } + public void setSpecialCategory(String v) { this.specialCategory = v; } + + public String getWeakerSection() { return weakerSection; } + public void setWeakerSection(String v) { this.weakerSection = v; } + + public String getGuardianName() { return guardianName; } + public void setGuardianName(String v) { this.guardianName = v; } + + public String getGuardianCifNumber() { return guardianCifNumber; } + public void setGuardianCifNumber(String v) { this.guardianCifNumber = v; } + + public String getGuardianRelationship() { return guardianRelationship; } + public void setGuardianRelationship(String v) { this.guardianRelationship = v; } + + public LocalDate getMinorDateOfBirth() { return minorDateOfBirth; } + public void setMinorDateOfBirth(LocalDate v) { this.minorDateOfBirth = v; } + + public Long getGuardianClientId() { return guardianClientId; } + public void setGuardianClientId(Long v) { this.guardianClientId = v; } + + public Long getIntroducerClientId() { return introducerClientId; } + public void setIntroducerClientId(Long v) { this.introducerClientId = v; } + + public String getIntroducerName() { return introducerName; } + public void setIntroducerName(String v) { this.introducerName = v; } + + public String getIntroducerCifNumber() { return introducerCifNumber; } + public void setIntroducerCifNumber(String v) { this.introducerCifNumber = v; } + + public String getNomineeData() { return nomineeData; } + public void setNomineeData(String v) { this.nomineeData = v; } + + public Long getLinkedAccountId() { return linkedAccountId; } + public void setLinkedAccountId(Long v) { this.linkedAccountId = v; } + + public String getLinkedAccountType() { return linkedAccountType; } + public void setLinkedAccountType(String v) { this.linkedAccountType = v; } + + public String getLinkedAccountNo() { return linkedAccountNo; } + public void setLinkedAccountNo(String v) { this.linkedAccountNo = v; } + + public SavingsAccountFacilityConfig getFacilityConfig() { return facilityConfig; } + public void setFacilityConfig(SavingsAccountFacilityConfig v) { this.facilityConfig = v; } + + public List getJointHolders() { return jointHolders; } + public void setJointHolders(List v) { this.jointHolders = v; } + + public Boolean getNominationFacility() { return nominationFacility; } + public void setNominationFacility(Boolean v) { this.nominationFacility = v; } + + public Boolean getAutoDebitFacility() { return autoDebitFacility; } + public void setAutoDebitFacility(Boolean v) { this.autoDebitFacility = v; } +} From ce0fb18eeb0cd90325f05c2f5798de637d7b6bca Mon Sep 17 00:00:00 2001 From: Jaanu423 Date: Thu, 21 May 2026 11:20:30 +0530 Subject: [PATCH 2/3] feat: implement full current account transaction write service - New CurrentAccountTransactionWritePlatformService interface with deposit, withdrawal, holdAmount, releaseAmount, undoTransaction, reverseTransaction, and adjustTransaction operations - New CurrentAccountTransactionWritePlatformServiceImpl using m_current_account tables directly (not m_savings_account), with GL journal entry posting - Add holdAmount, releaseAmount, reversal factory methods to CurrentAccountTransaction - Wire all 7 current account transaction command handlers to new write service - Fix CurrentAccountTransactionsApiResource template SQL column: currency_multiplesof - Fix account number search in SearchParameters Co-Authored-By: Claude Sonnet 4.6 --- .../core/service/SearchParameters.java | 289 +- .../domain/CurrentAccountTransaction.java | 342 +- .../CurrentAccountReadPlatformService.java | 60 +- ...ccountTransactionWritePlatformService.java | 39 + ...CurrentAccountTransactionsApiResource.java | 57 + .../api/CurrentAccountsApiResource.java | 430 +- .../api/FixedDepositAccountsApiResource.java | 1042 ++--- .../RecurringDepositAccountsApiResource.java | 942 ++-- .../api/SavingsAccountsApiResource.java | 1289 +++--- ...ntTransactionAdjustmentCommandHandler.java | 8 +- .../DepositCurrentAccountCommandHandler.java | 6 +- ...oldAmountCurrentAccountCommandHandler.java | 6 +- ...aseAmountCurrentAccountCommandHandler.java | 6 +- ...ansactionCurrentAccountCommandHandler.java | 6 +- ...ansactionCurrentAccountCommandHandler.java | 6 +- .../WithdrawCurrentAccountCommandHandler.java | 6 +- ...CurrentAccountReadPlatformServiceImpl.java | 1106 ++--- ...ntTransactionWritePlatformServiceImpl.java | 468 ++ .../DepositAccountReadPlatformService.java | 115 +- ...DepositAccountReadPlatformServiceImpl.java | 3912 +++++++++-------- ...SavingsAccountReadPlatformServiceImpl.java | 3078 ++++++------- 21 files changed, 6961 insertions(+), 6252 deletions(-) create mode 100644 fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentaccount/service/CurrentAccountTransactionWritePlatformService.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/CurrentAccountTransactionWritePlatformServiceImpl.java diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/SearchParameters.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/SearchParameters.java index 2f09780ab..e9b83b854 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/SearchParameters.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/SearchParameters.java @@ -1,144 +1,145 @@ -/** - * 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.infrastructure.core.service; - -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import org.apache.commons.lang3.StringUtils; - -@Builder -@Getter -@AllArgsConstructor -public class SearchParameters { - - public static final int DEFAULT_MAX_LIMIT = 200; - - private Long officeId; - private String externalId; - private String name; - private String hierarchy; - private String firstname; - private String lastname; - private String status; - private Integer offset; - @Getter(AccessLevel.NONE) - private Integer limit; - private String orderBy; - private String sortOrder; - private String accountNo; - private String currencyCode; - private Long staffId; - private Long loanId; - private Long clientId; - private Long savingsId; - @Getter(AccessLevel.NONE) - private Boolean orphansOnly; - private Long provisioningEntryId; - private Long productId; - private Long categoryId; - @Getter(AccessLevel.NONE) - private Boolean isSelfUser; - private Integer legalForm; - - public Integer getLimit() { - if (limit == null) { - return DEFAULT_MAX_LIMIT; - } - - if (limit > 0) { - return limit; - } - - return null; // unlimited (0 or less) - } - - public Boolean getOrphansOnly() { - return Boolean.TRUE.equals(orphansOnly); - } - - public Boolean getIsSelfUser() { - return Boolean.TRUE.equals(isSelfUser); - } - - public boolean hasOrderBy() { - return StringUtils.isNotBlank(this.orderBy); - } - - public boolean hasSortOrder() { - return StringUtils.isNotBlank(this.sortOrder); - } - - public boolean hasOfficeId() { - return this.officeId != null && this.officeId != 0; - } - - public boolean hasCurrencyCode() { - return StringUtils.isNotBlank(this.currencyCode); - } - - public boolean hasLimit() { - return this.limit != null && this.limit > 0; - } - - public boolean hasOffset() { - return this.offset != null; - } - - public boolean hasHierarchy() { - return StringUtils.isNotBlank(this.hierarchy); - } - - public boolean hasStaffId() { - return this.staffId != null && this.staffId != 0; - } - - public boolean hasLoanId() { - return this.loanId != null && this.loanId != 0; - } - - public boolean hasSavingsId() { - return this.savingsId != null && this.savingsId != 0; - } - - public boolean hasProvisioningEntryId() { - return this.provisioningEntryId != null && this.provisioningEntryId != 0; - } - - public boolean hasProductId() { - return this.productId != null && this.productId != 0; - } - - public boolean hasCategoryId() { - return this.categoryId != null && this.categoryId != 0; - } - - public boolean isPerson() { - return this.legalForm != null && this.legalForm == 1; - } - - public boolean isEntity() { - return this.legalForm != null && this.legalForm == 2; - } - - public boolean hasLegalForm() { - return this.legalForm != null; - } -} +/** + * 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.infrastructure.core.service; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import org.apache.commons.lang3.StringUtils; + +@Builder +@Getter +@AllArgsConstructor +public class SearchParameters { + + public static final int DEFAULT_MAX_LIMIT = 200; + + private Long officeId; + private String externalId; + private String name; + private String hierarchy; + private String firstname; + private String lastname; + private String status; + private Integer offset; + @Getter(AccessLevel.NONE) + private Integer limit; + private String orderBy; + private String sortOrder; + private String accountNo; + private Integer depositType; + private String currencyCode; + private Long staffId; + private Long loanId; + private Long clientId; + private Long savingsId; + @Getter(AccessLevel.NONE) + private Boolean orphansOnly; + private Long provisioningEntryId; + private Long productId; + private Long categoryId; + @Getter(AccessLevel.NONE) + private Boolean isSelfUser; + private Integer legalForm; + + public Integer getLimit() { + if (limit == null) { + return DEFAULT_MAX_LIMIT; + } + + if (limit > 0) { + return limit; + } + + return null; // unlimited (0 or less) + } + + public Boolean getOrphansOnly() { + return Boolean.TRUE.equals(orphansOnly); + } + + public Boolean getIsSelfUser() { + return Boolean.TRUE.equals(isSelfUser); + } + + public boolean hasOrderBy() { + return StringUtils.isNotBlank(this.orderBy); + } + + public boolean hasSortOrder() { + return StringUtils.isNotBlank(this.sortOrder); + } + + public boolean hasOfficeId() { + return this.officeId != null && this.officeId != 0; + } + + public boolean hasCurrencyCode() { + return StringUtils.isNotBlank(this.currencyCode); + } + + public boolean hasLimit() { + return this.limit != null && this.limit > 0; + } + + public boolean hasOffset() { + return this.offset != null; + } + + public boolean hasHierarchy() { + return StringUtils.isNotBlank(this.hierarchy); + } + + public boolean hasStaffId() { + return this.staffId != null && this.staffId != 0; + } + + public boolean hasLoanId() { + return this.loanId != null && this.loanId != 0; + } + + public boolean hasSavingsId() { + return this.savingsId != null && this.savingsId != 0; + } + + public boolean hasProvisioningEntryId() { + return this.provisioningEntryId != null && this.provisioningEntryId != 0; + } + + public boolean hasProductId() { + return this.productId != null && this.productId != 0; + } + + public boolean hasCategoryId() { + return this.categoryId != null && this.categoryId != 0; + } + + public boolean isPerson() { + return this.legalForm != null && this.legalForm == 1; + } + + public boolean isEntity() { + return this.legalForm != null && this.legalForm == 2; + } + + public boolean hasLegalForm() { + return this.legalForm != null; + } +} diff --git a/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentaccount/domain/CurrentAccountTransaction.java b/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentaccount/domain/CurrentAccountTransaction.java index f85a7242b..60f4eb073 100644 --- a/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentaccount/domain/CurrentAccountTransaction.java +++ b/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentaccount/domain/CurrentAccountTransaction.java @@ -1,149 +1,193 @@ -/** - * 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.currentaccount.domain; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.Table; -import java.math.BigDecimal; -import java.time.LocalDate; -import java.time.LocalDateTime; -import lombok.Getter; -import lombok.Setter; -import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; -import org.apache.fineract.organisation.office.domain.Office; - -@Entity -@Table(name = "m_current_account_transaction") -@Getter -@Setter -public class CurrentAccountTransaction extends AbstractPersistableCustom { - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "current_account_id", nullable = false) - private CurrentAccount currentAccount; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "office_id", nullable = false) - private Office office; - - // 1=Deposit, 2=Withdrawal, 3=InterestPosting, 4=Charge, 5=ChargeWaiver, 6=Transfer, 7=Reversal - @Column(name = "transaction_type_enum", nullable = false) - private Integer transactionTypeEnum; - - @Column(name = "transaction_date", nullable = false) - private LocalDate transactionDate; - - @Column(name = "amount", nullable = false, precision = 19, scale = 6) - private BigDecimal amount; - - @Column(name = "ref_no", length = 100) - private String refNo; - - @Column(name = "remarks", length = 500) - private String remarks; - - @Column(name = "is_reversed", nullable = false) - private boolean reversed = false; - - @Column(name = "is_reversal", nullable = false) - private boolean reversal = false; - - @Column(name = "original_transaction_id") - private Long originalTransactionId; - - @Column(name = "is_manual", nullable = false) - private boolean manual = false; - - @Column(name = "running_balance_derived", precision = 19, scale = 6) - private BigDecimal runningBalanceDerived; - - @Column(name = "cumulative_balance_derived", precision = 19, scale = 6) - private BigDecimal cumulativeBalanceDerived; - - @Column(name = "balance_end_date_derived") - private LocalDate balanceEndDateDerived; - - @Column(name = "balance_number_of_days_derived") - private Integer balanceNumberOfDaysDerived; - - @Column(name = "overdraft_amount_derived", precision = 19, scale = 6) - private BigDecimal overdraftAmountDerived; - - @Column(name = "is_lien_transaction", nullable = false) - private boolean lienTransaction = false; - - @Column(name = "release_id_of_hold_amount_transaction") - private Long releaseIdOfHoldAmountTransaction; - - @Column(name = "reason_for_block", length = 200) - private String reasonForBlock; - - @Column(name = "submitted_on_date") - private LocalDate submittedOnDate; - - @Column(name = "created_date") - private LocalDateTime createdDate; - - @Column(name = "created_by") - private Long createdBy; - - protected CurrentAccountTransaction() {} - - public static CurrentAccountTransaction deposit(CurrentAccount account, Office office, - BigDecimal amount, LocalDate transactionDate, String refNo) { - CurrentAccountTransaction txn = new CurrentAccountTransaction(); - txn.currentAccount = account; - txn.office = office; - txn.transactionTypeEnum = 1; - txn.amount = amount; - txn.transactionDate = transactionDate; - txn.refNo = refNo; - txn.submittedOnDate = transactionDate; - return txn; - } - - public static CurrentAccountTransaction chargeDeduction(CurrentAccount account, Office office, - BigDecimal amount, LocalDate transactionDate) { - CurrentAccountTransaction txn = new CurrentAccountTransaction(); - txn.currentAccount = account; - txn.office = office; - txn.transactionTypeEnum = 7; // PAY_CHARGE - txn.amount = amount; - txn.transactionDate = transactionDate; - txn.submittedOnDate = transactionDate; - return txn; - } - - public static CurrentAccountTransaction withdrawal(CurrentAccount account, Office office, - BigDecimal amount, LocalDate transactionDate, String refNo) { - CurrentAccountTransaction txn = new CurrentAccountTransaction(); - txn.currentAccount = account; - txn.office = office; - txn.transactionTypeEnum = 2; - txn.amount = amount; - txn.transactionDate = transactionDate; - txn.refNo = refNo; - txn.submittedOnDate = transactionDate; - return txn; - } -} +/** + * 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.currentaccount.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import lombok.Getter; +import lombok.Setter; +import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; +import org.apache.fineract.organisation.office.domain.Office; + +@Entity +@Table(name = "m_current_account_transaction") +@Getter +@Setter +public class CurrentAccountTransaction extends AbstractPersistableCustom { + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "current_account_id", nullable = false) + private CurrentAccount currentAccount; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "office_id", nullable = false) + private Office office; + + // 1=Deposit, 2=Withdrawal, 3=InterestPosting, 4=Charge, 5=ChargeWaiver, 6=Transfer, 7=Reversal + @Column(name = "transaction_type_enum", nullable = false) + private Integer transactionTypeEnum; + + @Column(name = "transaction_date", nullable = false) + private LocalDate transactionDate; + + @Column(name = "amount", nullable = false, precision = 19, scale = 6) + private BigDecimal amount; + + @Column(name = "ref_no", length = 100) + private String refNo; + + @Column(name = "remarks", length = 500) + private String remarks; + + @Column(name = "is_reversed", nullable = false) + private boolean reversed = false; + + @Column(name = "is_reversal", nullable = false) + private boolean reversal = false; + + @Column(name = "original_transaction_id") + private Long originalTransactionId; + + @Column(name = "is_manual", nullable = false) + private boolean manual = false; + + @Column(name = "running_balance_derived", precision = 19, scale = 6) + private BigDecimal runningBalanceDerived; + + @Column(name = "cumulative_balance_derived", precision = 19, scale = 6) + private BigDecimal cumulativeBalanceDerived; + + @Column(name = "balance_end_date_derived") + private LocalDate balanceEndDateDerived; + + @Column(name = "balance_number_of_days_derived") + private Integer balanceNumberOfDaysDerived; + + @Column(name = "overdraft_amount_derived", precision = 19, scale = 6) + private BigDecimal overdraftAmountDerived; + + @Column(name = "is_lien_transaction", nullable = false) + private boolean lienTransaction = false; + + @Column(name = "release_id_of_hold_amount_transaction") + private Long releaseIdOfHoldAmountTransaction; + + @Column(name = "reason_for_block", length = 200) + private String reasonForBlock; + + @Column(name = "submitted_on_date") + private LocalDate submittedOnDate; + + @Column(name = "created_date") + private LocalDateTime createdDate; + + @Column(name = "created_by") + private Long createdBy; + + protected CurrentAccountTransaction() {} + + public static CurrentAccountTransaction deposit(CurrentAccount account, Office office, + BigDecimal amount, LocalDate transactionDate, String refNo) { + CurrentAccountTransaction txn = new CurrentAccountTransaction(); + txn.currentAccount = account; + txn.office = office; + txn.transactionTypeEnum = 1; + txn.amount = amount; + txn.transactionDate = transactionDate; + txn.refNo = refNo; + txn.submittedOnDate = transactionDate; + return txn; + } + + public static CurrentAccountTransaction chargeDeduction(CurrentAccount account, Office office, + BigDecimal amount, LocalDate transactionDate) { + CurrentAccountTransaction txn = new CurrentAccountTransaction(); + txn.currentAccount = account; + txn.office = office; + txn.transactionTypeEnum = 7; // PAY_CHARGE + txn.amount = amount; + txn.transactionDate = transactionDate; + txn.submittedOnDate = transactionDate; + return txn; + } + + public static CurrentAccountTransaction withdrawal(CurrentAccount account, Office office, + BigDecimal amount, LocalDate transactionDate, String refNo) { + CurrentAccountTransaction txn = new CurrentAccountTransaction(); + txn.currentAccount = account; + txn.office = office; + txn.transactionTypeEnum = 2; + txn.amount = amount; + txn.transactionDate = transactionDate; + txn.refNo = refNo; + txn.submittedOnDate = transactionDate; + return txn; + } + + // transactionTypeEnum = 8 (Hold Amount) + public static CurrentAccountTransaction holdAmount(CurrentAccount account, Office office, + BigDecimal amount, LocalDate transactionDate, String reasonForBlock) { + CurrentAccountTransaction txn = new CurrentAccountTransaction(); + txn.currentAccount = account; + txn.office = office; + txn.transactionTypeEnum = 8; + txn.amount = amount; + txn.transactionDate = transactionDate; + txn.lienTransaction = true; + txn.reasonForBlock = reasonForBlock; + txn.submittedOnDate = transactionDate; + return txn; + } + + // transactionTypeEnum = 9 (Release Amount) + public static CurrentAccountTransaction releaseAmount(CurrentAccount account, Office office, + BigDecimal amount, LocalDate transactionDate, Long holdTransactionId) { + CurrentAccountTransaction txn = new CurrentAccountTransaction(); + txn.currentAccount = account; + txn.office = office; + txn.transactionTypeEnum = 9; + txn.amount = amount; + txn.transactionDate = transactionDate; + txn.releaseIdOfHoldAmountTransaction = holdTransactionId; + txn.submittedOnDate = transactionDate; + return txn; + } + + // transactionTypeEnum = 7 (Reversal / Undo) + public static CurrentAccountTransaction reversal(CurrentAccount account, Office office, + BigDecimal amount, LocalDate transactionDate, Long originalTransactionId) { + CurrentAccountTransaction txn = new CurrentAccountTransaction(); + txn.currentAccount = account; + txn.office = office; + txn.transactionTypeEnum = 7; + txn.amount = amount; + txn.transactionDate = transactionDate; + txn.reversal = true; + txn.originalTransactionId = originalTransactionId; + txn.submittedOnDate = transactionDate; + return txn; + } +} diff --git a/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentaccount/service/CurrentAccountReadPlatformService.java b/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentaccount/service/CurrentAccountReadPlatformService.java index ebc85ed53..bda1ddc5b 100644 --- a/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentaccount/service/CurrentAccountReadPlatformService.java +++ b/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentaccount/service/CurrentAccountReadPlatformService.java @@ -1,29 +1,31 @@ -/** - * 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.currentaccount.service; - -import java.util.List; -import java.util.Map; - -public interface CurrentAccountReadPlatformService { - - Map retrieveOne(Long accountId); - - List> retrieveAll(); -} +/** + * 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.currentaccount.service; + +import java.util.List; +import java.util.Map; + +public interface CurrentAccountReadPlatformService { + + Map retrieveOne(Long accountId); + + List> retrieveAll(); + + List> retrieveAll(Integer offset, Integer limit, String accountNo, String clientName); +} diff --git a/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentaccount/service/CurrentAccountTransactionWritePlatformService.java b/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentaccount/service/CurrentAccountTransactionWritePlatformService.java new file mode 100644 index 000000000..d860b7fb0 --- /dev/null +++ b/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentaccount/service/CurrentAccountTransactionWritePlatformService.java @@ -0,0 +1,39 @@ +/** + * 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.currentaccount.service; + +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; + +public interface CurrentAccountTransactionWritePlatformService { + + CommandProcessingResult deposit(Long accountId, JsonCommand command); + + CommandProcessingResult withdrawal(Long accountId, JsonCommand command); + + CommandProcessingResult holdAmount(Long accountId, JsonCommand command); + + CommandProcessingResult releaseAmount(Long accountId, Long transactionId); + + CommandProcessingResult undoTransaction(Long accountId, Long transactionId, boolean isReversal); + + CommandProcessingResult reverseTransaction(Long accountId, Long transactionId, boolean isReversal, JsonCommand command); + + CommandProcessingResult adjustTransaction(Long accountId, Long transactionId, JsonCommand command); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/CurrentAccountTransactionsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/CurrentAccountTransactionsApiResource.java index f5b15b94e..36bf1ecd5 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/CurrentAccountTransactionsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/CurrentAccountTransactionsApiResource.java @@ -18,6 +18,8 @@ */ package org.apache.fineract.portfolio.savings.api; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -32,6 +34,10 @@ 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.Collection; +import java.util.LinkedHashMap; +import java.util.Map; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; import org.apache.fineract.commands.domain.CommandWrapper; @@ -43,10 +49,14 @@ import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamE import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings; import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.portfolio.currentaccount.exception.CurrentAccountNotFoundException; +import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData; +import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeReadService; import org.apache.fineract.portfolio.savings.DepositsApiConstants; import org.apache.fineract.portfolio.savings.DepositAccountType; import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData; import org.apache.fineract.portfolio.savings.service.SavingsAccountReadPlatformService; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Component; @Path("/v1/currentaccounts/{accountId}/transactions") @@ -55,11 +65,58 @@ import org.springframework.stereotype.Component; @RequiredArgsConstructor public class CurrentAccountTransactionsApiResource { + private static final Gson GSON = new GsonBuilder().serializeNulls().create(); + private final PlatformSecurityContext context; private final DefaultToApiJsonSerializer toApiJsonSerializer; private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; private final ApiRequestParameterHelper apiRequestParameterHelper; private final SavingsAccountReadPlatformService savingsAccountReadPlatformService; + private final PaymentTypeReadService paymentTypeReadService; + private final JdbcTemplate jdbcTemplate; + + @GET + @Path("template") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Retrieve current account transaction template") + @ApiResponse(responseCode = "200", description = "OK") + public String retrieveTemplate(@PathParam("accountId") final Long accountId, @Context final UriInfo uriInfo) { + + context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.CURRENT_ACCOUNT_RESOURCE_NAME); + + final String sql = "SELECT ca.currency_code, ca.currency_digits, ca.currency_multiplesof, " + + "mc.display_symbol, mc.name AS currency_name, mc.internationalized_name_code " + + "FROM m_current_account ca " + + "LEFT JOIN m_currency mc ON mc.code = ca.currency_code " + + "WHERE ca.id = ?"; + + final Map template; + try { + template = jdbcTemplate.queryForObject(sql, (rs, rowNum) -> { + final Map map = new LinkedHashMap<>(); + final LocalDate today = LocalDate.now(); + map.put("date", new int[] { today.getYear(), today.getMonthValue(), today.getDayOfMonth() }); + + final Map currency = new LinkedHashMap<>(); + currency.put("code", rs.getString("currency_code")); + currency.put("decimalPlaces", rs.getInt("currency_digits")); + currency.put("inMultiplesOf", rs.getInt("currency_multiplesof")); + currency.put("displaySymbol", rs.getString("display_symbol")); + currency.put("name", rs.getString("currency_name")); + currency.put("nameCode", rs.getString("internationalized_name_code")); + map.put("currency", currency); + return map; + }, accountId); + } catch (org.springframework.dao.EmptyResultDataAccessException e) { + throw new CurrentAccountNotFoundException(accountId); + } + + final Collection paymentTypeOptions = paymentTypeReadService.retrieveAllPaymentTypes(); + template.put("paymentTypeOptions", paymentTypeOptions); + + return GSON.toJson(template); + } @GET @Path("{transactionId}") diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/CurrentAccountsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/CurrentAccountsApiResource.java index 4991170d2..6da4821dc 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/CurrentAccountsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/CurrentAccountsApiResource.java @@ -1,214 +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.savings.api; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.DELETE; -import jakarta.ws.rs.DefaultValue; -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 com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonParser; -import java.util.List; -import java.util.Map; -import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.StringUtils; -import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper; -import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; -import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException; -import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings; -import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; -import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; -import org.apache.fineract.infrastructure.security.service.SqlValidator; -import org.apache.fineract.portfolio.savings.DepositsApiConstants; -import org.apache.fineract.portfolio.savings.data.SavingsAccountData; -import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountReadPlatformService; -import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountWritePlatformService; -import org.apache.fineract.portfolio.savings.service.CurrentAccountTemplateReadPlatformService; -import org.springframework.stereotype.Component; - -@Path("/v1/currentaccounts") -@Component -@Tag(name = "Current Account", description = "Current accounts are instances of a particular current product created for an individual or group.") -@RequiredArgsConstructor -public class CurrentAccountsApiResource { - - private static final Gson GSON = new GsonBuilder().serializeNulls().create(); - - private final CurrentAccountTemplateReadPlatformService currentAccountTemplateReadPlatformService; - private final PlatformSecurityContext context; - private final DefaultToApiJsonSerializer toApiJsonSerializer; - private final ApiRequestParameterHelper apiRequestParameterHelper; - private final SqlValidator sqlValidator; - private final CurrentAccountWritePlatformService currentAccountWritePlatformService; - private final CurrentAccountReadPlatformService currentAccountReadPlatformService; - - @GET - @Path("template") - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - @Operation(summary = "Retrieve Current Account Template") - @ApiResponse(responseCode = "200", description = "OK") - public String template(@QueryParam("clientId") @Parameter(description = "clientId") final Long clientId, - @QueryParam("groupId") @Parameter(description = "groupId") final Long groupId, - @QueryParam("productId") @Parameter(description = "productId") final Long productId, - @DefaultValue("false") @QueryParam("staffInSelectedOfficeOnly") final boolean staffInSelectedOfficeOnly, - @Context final UriInfo uriInfo) { - - context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.CURRENT_ACCOUNT_RESOURCE_NAME); - final SavingsAccountData savingsAccount = currentAccountTemplateReadPlatformService.retrieveTemplate(clientId, groupId, productId, - staffInSelectedOfficeOnly); - final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters()); - return toApiJsonSerializer.serialize(settings, savingsAccount, SavingsApiSetConstants.SAVINGS_ACCOUNT_RESPONSE_DATA_PARAMETERS); - } - - @GET - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - @Operation(summary = "List current accounts") - @ApiResponse(responseCode = "200", description = "OK") - public String retrieveAll(@Context final UriInfo uriInfo, - @QueryParam("externalId") final String externalId, - @QueryParam("offset") final Integer offset, - @QueryParam("limit") final Integer limit, - @QueryParam("orderBy") final String orderBy, - @QueryParam("sortOrder") final String sortOrder) { - - context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.CURRENT_ACCOUNT_RESOURCE_NAME); - final List> accounts = currentAccountReadPlatformService.retrieveAll(); - return GSON.toJson(accounts); - } - - @POST - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - @Operation(summary = "Submit new current account application") - @ApiResponse(responseCode = "200", description = "OK") - public String submitApplication(@Parameter(hidden = true) final String apiRequestBodyAsJson) { - final org.apache.fineract.infrastructure.core.api.JsonCommand command = - org.apache.fineract.infrastructure.core.api.JsonCommand.fromJsonElement(null, - JsonParser.parseString(apiRequestBodyAsJson != null ? apiRequestBodyAsJson : "{}")); - final CommandProcessingResult result = currentAccountWritePlatformService.submitApplication(command); - return toApiJsonSerializer.serialize(result); - } - - @GET - @Path("{accountId}") - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - @Operation(summary = "Retrieve a current account") - @ApiResponse(responseCode = "200", description = "OK") - public String retrieveOne(@PathParam("accountId") final Long accountId, - @DefaultValue("false") @QueryParam("staffInSelectedOfficeOnly") final boolean staffInSelectedOfficeOnly, - @DefaultValue("all") @QueryParam("chargeStatus") final String chargeStatus, - @Context final UriInfo uriInfo) { - - context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.CURRENT_ACCOUNT_RESOURCE_NAME); - final Map account = currentAccountReadPlatformService.retrieveOne(accountId); - return GSON.toJson(account); - } - - @PUT - @Path("{accountId}") - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - @Operation(summary = "Modify a current account application") - @ApiResponse(responseCode = "200", description = "OK") - public String update(@PathParam("accountId") final Long accountId, - @Parameter(hidden = true) final String apiRequestBodyAsJson, - @QueryParam("command") final String commandParam) { - - final org.apache.fineract.infrastructure.core.api.JsonCommand command = - org.apache.fineract.infrastructure.core.api.JsonCommand.fromJsonElement(null, - JsonParser.parseString(apiRequestBodyAsJson != null ? apiRequestBodyAsJson : "{}")); - final CommandProcessingResult result = currentAccountWritePlatformService.modifyApplication(accountId, command); - return toApiJsonSerializer.serialize(result); - } - - @POST - @Path("{accountId}") - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - @Operation(summary = "Approve/Reject/Activate/Close/Deposit/Withdrawal and other current account commands") - @ApiResponse(responseCode = "200", description = "OK") - public String handleCommands(@PathParam("accountId") final Long accountId, - @QueryParam("command") final String commandParam, - @Parameter(hidden = true) final String apiRequestBodyAsJson) { - - String jsonApiRequest = apiRequestBodyAsJson; - if (StringUtils.isBlank(jsonApiRequest)) { - jsonApiRequest = "{}"; - } - - final org.apache.fineract.infrastructure.core.api.JsonCommand command = - org.apache.fineract.infrastructure.core.api.JsonCommand.fromJsonElement(null, - JsonParser.parseString(jsonApiRequest)); - - CommandProcessingResult result = null; - if (is(commandParam, "reject")) { - result = currentAccountWritePlatformService.rejectApplication(accountId, command); - } else if (is(commandParam, "withdrawnByApplicant")) { - result = currentAccountWritePlatformService.withdrawApplication(accountId, command); - } else if (is(commandParam, "approve")) { - result = currentAccountWritePlatformService.approveApplication(accountId, command); - } else if (is(commandParam, "undoapproval")) { - result = currentAccountWritePlatformService.undoApproval(accountId, command); - } else if (is(commandParam, "activate")) { - result = currentAccountWritePlatformService.activate(accountId, command); - } else if (is(commandParam, "close")) { - result = currentAccountWritePlatformService.closeAccount(accountId, command); - } - - if (result == null) { - throw new UnrecognizedQueryParamException("command", commandParam, - new Object[] { "reject", "withdrawnByApplicant", "approve", "undoapproval", "activate", "close" }); - } - - return toApiJsonSerializer.serialize(result); - } - - @DELETE - @Path("{accountId}") - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - @Operation(summary = "Delete a current account application") - @ApiResponse(responseCode = "200", description = "OK") - public String delete(@PathParam("accountId") final Long accountId) { - final CommandProcessingResult result = currentAccountWritePlatformService.deleteApplication(accountId); - return toApiJsonSerializer.serialize(result); - } - - private boolean is(final String commandParam, final String commandValue) { - return StringUtils.isNotBlank(commandParam) && commandParam.trim().equalsIgnoreCase(commandValue); - } - -} +/** + * 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.api; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.DefaultValue; +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 com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonParser; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException; +import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings; +import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.infrastructure.security.service.SqlValidator; +import org.apache.fineract.portfolio.savings.DepositsApiConstants; +import org.apache.fineract.portfolio.savings.data.SavingsAccountData; +import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountReadPlatformService; +import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountWritePlatformService; +import org.apache.fineract.portfolio.savings.service.CurrentAccountTemplateReadPlatformService; +import org.springframework.stereotype.Component; + +@Path("/v1/currentaccounts") +@Component +@Tag(name = "Current Account", description = "Current accounts are instances of a particular current product created for an individual or group.") +@RequiredArgsConstructor +public class CurrentAccountsApiResource { + + private static final Gson GSON = new GsonBuilder().serializeNulls().create(); + + private final CurrentAccountTemplateReadPlatformService currentAccountTemplateReadPlatformService; + private final PlatformSecurityContext context; + private final DefaultToApiJsonSerializer toApiJsonSerializer; + private final ApiRequestParameterHelper apiRequestParameterHelper; + private final SqlValidator sqlValidator; + private final CurrentAccountWritePlatformService currentAccountWritePlatformService; + private final CurrentAccountReadPlatformService currentAccountReadPlatformService; + + @GET + @Path("template") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Retrieve Current Account Template") + @ApiResponse(responseCode = "200", description = "OK") + public String template(@QueryParam("clientId") @Parameter(description = "clientId") final Long clientId, + @QueryParam("groupId") @Parameter(description = "groupId") final Long groupId, + @QueryParam("productId") @Parameter(description = "productId") final Long productId, + @DefaultValue("false") @QueryParam("staffInSelectedOfficeOnly") final boolean staffInSelectedOfficeOnly, + @Context final UriInfo uriInfo) { + + context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.CURRENT_ACCOUNT_RESOURCE_NAME); + final SavingsAccountData savingsAccount = currentAccountTemplateReadPlatformService.retrieveTemplate(clientId, groupId, productId, + staffInSelectedOfficeOnly); + final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + return toApiJsonSerializer.serialize(settings, savingsAccount, SavingsApiSetConstants.SAVINGS_ACCOUNT_RESPONSE_DATA_PARAMETERS); + } + + @GET + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "List current accounts") + @ApiResponse(responseCode = "200", description = "OK") + public String retrieveAll(@Context final UriInfo uriInfo, + @QueryParam("externalId") final String externalId, + @QueryParam("offset") final Integer offset, + @QueryParam("limit") final Integer limit, + @QueryParam("orderBy") final String orderBy, + @QueryParam("sortOrder") final String sortOrder, + @QueryParam("accountNo") @Parameter(description = "accountNo") final String accountNo, + @QueryParam("clientName") @Parameter(description = "clientName") final String clientName) { + + context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.CURRENT_ACCOUNT_RESOURCE_NAME); + final List> accounts = currentAccountReadPlatformService.retrieveAll(offset, limit, accountNo, clientName); + return GSON.toJson(accounts); + } + + @POST + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Submit new current account application") + @ApiResponse(responseCode = "200", description = "OK") + public String submitApplication(@Parameter(hidden = true) final String apiRequestBodyAsJson) { + final org.apache.fineract.infrastructure.core.api.JsonCommand command = + org.apache.fineract.infrastructure.core.api.JsonCommand.fromJsonElement(null, + JsonParser.parseString(apiRequestBodyAsJson != null ? apiRequestBodyAsJson : "{}")); + final CommandProcessingResult result = currentAccountWritePlatformService.submitApplication(command); + return toApiJsonSerializer.serialize(result); + } + + @GET + @Path("{accountId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Retrieve a current account") + @ApiResponse(responseCode = "200", description = "OK") + public String retrieveOne(@PathParam("accountId") final Long accountId, + @DefaultValue("false") @QueryParam("staffInSelectedOfficeOnly") final boolean staffInSelectedOfficeOnly, + @DefaultValue("all") @QueryParam("chargeStatus") final String chargeStatus, + @Context final UriInfo uriInfo) { + + context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.CURRENT_ACCOUNT_RESOURCE_NAME); + final Map account = currentAccountReadPlatformService.retrieveOne(accountId); + return GSON.toJson(account); + } + + @PUT + @Path("{accountId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Modify a current account application") + @ApiResponse(responseCode = "200", description = "OK") + public String update(@PathParam("accountId") final Long accountId, + @Parameter(hidden = true) final String apiRequestBodyAsJson, + @QueryParam("command") final String commandParam) { + + final org.apache.fineract.infrastructure.core.api.JsonCommand command = + org.apache.fineract.infrastructure.core.api.JsonCommand.fromJsonElement(null, + JsonParser.parseString(apiRequestBodyAsJson != null ? apiRequestBodyAsJson : "{}")); + final CommandProcessingResult result = currentAccountWritePlatformService.modifyApplication(accountId, command); + return toApiJsonSerializer.serialize(result); + } + + @POST + @Path("{accountId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Approve/Reject/Activate/Close/Deposit/Withdrawal and other current account commands") + @ApiResponse(responseCode = "200", description = "OK") + public String handleCommands(@PathParam("accountId") final Long accountId, + @QueryParam("command") final String commandParam, + @Parameter(hidden = true) final String apiRequestBodyAsJson) { + + String jsonApiRequest = apiRequestBodyAsJson; + if (StringUtils.isBlank(jsonApiRequest)) { + jsonApiRequest = "{}"; + } + + final org.apache.fineract.infrastructure.core.api.JsonCommand command = + org.apache.fineract.infrastructure.core.api.JsonCommand.fromJsonElement(null, + JsonParser.parseString(jsonApiRequest)); + + CommandProcessingResult result = null; + if (is(commandParam, "reject")) { + result = currentAccountWritePlatformService.rejectApplication(accountId, command); + } else if (is(commandParam, "withdrawnByApplicant")) { + result = currentAccountWritePlatformService.withdrawApplication(accountId, command); + } else if (is(commandParam, "approve")) { + result = currentAccountWritePlatformService.approveApplication(accountId, command); + } else if (is(commandParam, "undoapproval")) { + result = currentAccountWritePlatformService.undoApproval(accountId, command); + } else if (is(commandParam, "activate")) { + result = currentAccountWritePlatformService.activate(accountId, command); + } else if (is(commandParam, "close")) { + result = currentAccountWritePlatformService.closeAccount(accountId, command); + } + + if (result == null) { + throw new UnrecognizedQueryParamException("command", commandParam, + new Object[] { "reject", "withdrawnByApplicant", "approve", "undoapproval", "activate", "close" }); + } + + return toApiJsonSerializer.serialize(result); + } + + @DELETE + @Path("{accountId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Delete a current account application") + @ApiResponse(responseCode = "200", description = "OK") + public String delete(@PathParam("accountId") final Long accountId) { + final CommandProcessingResult result = currentAccountWritePlatformService.deleteApplication(accountId); + return toApiJsonSerializer.serialize(result); + } + + private boolean is(final String commandParam, final String commandValue) { + return StringUtils.isNotBlank(commandParam) && commandParam.trim().equalsIgnoreCase(commandValue); + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/FixedDepositAccountsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/FixedDepositAccountsApiResource.java index 3c538c66f..ab67363e6 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/FixedDepositAccountsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/FixedDepositAccountsApiResource.java @@ -1,520 +1,522 @@ -/** - * 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.api; - -import com.google.gson.JsonElement; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.ArraySchema; -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.Consumes; -import jakarta.ws.rs.DELETE; -import jakarta.ws.rs.DefaultValue; -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.Response; -import jakarta.ws.rs.core.UriInfo; -import java.io.InputStream; -import java.math.BigDecimal; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Set; -import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.StringUtils; -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.bulkimport.data.GlobalEntityType; -import org.apache.fineract.infrastructure.bulkimport.service.BulkImportWorkbookPopulatorService; -import org.apache.fineract.infrastructure.bulkimport.service.BulkImportWorkbookService; -import org.apache.fineract.infrastructure.core.api.ApiParameterHelper; -import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper; -import org.apache.fineract.infrastructure.core.api.JsonQuery; -import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; -import org.apache.fineract.infrastructure.core.data.PaginationParameters; -import org.apache.fineract.infrastructure.core.data.UploadRequest; -import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException; -import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings; -import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; -import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; -import org.apache.fineract.infrastructure.core.service.Page; -import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; -import org.apache.fineract.infrastructure.security.service.SqlValidator; -import org.apache.fineract.portfolio.account.data.PortfolioAccountData; -import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlatformService; -import org.apache.fineract.portfolio.savings.DepositAccountType; -import org.apache.fineract.portfolio.savings.DepositsApiConstants; -import org.apache.fineract.portfolio.savings.SavingsApiConstants; -import org.apache.fineract.portfolio.savings.data.DepositAccountData; -import org.apache.fineract.portfolio.savings.data.FixedDepositAccountData; -import org.apache.fineract.portfolio.savings.data.SavingsAccountChargeData; -import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData; -import org.apache.fineract.portfolio.savings.service.DepositAccountPreMatureCalculationPlatformService; -import org.apache.fineract.portfolio.savings.service.DepositAccountReadPlatformService; -import org.apache.fineract.portfolio.savings.service.FixedDepositAccountInterestCalculationService; -import org.apache.fineract.portfolio.savings.service.SavingsAccountChargeReadPlatformService; -import org.glassfish.jersey.media.multipart.FormDataContentDisposition; -import org.glassfish.jersey.media.multipart.FormDataParam; -import org.springframework.stereotype.Component; -import org.springframework.util.CollectionUtils; - -@Path("/v1/fixeddepositaccounts") -@Component -@Tag(name = "Fixed Deposit Account", description = "Fixed Deposit accounts are instances of a praticular fixed deposit product created. An application process around the creation of accounts is also supported.") -@RequiredArgsConstructor -public class FixedDepositAccountsApiResource { - - private final DepositAccountReadPlatformService depositAccountReadPlatformService; - private final PlatformSecurityContext context; - private final DefaultToApiJsonSerializer toApiJsonSerializer; - private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; - private final ApiRequestParameterHelper apiRequestParameterHelper; - private final SavingsAccountChargeReadPlatformService savingsAccountChargeReadPlatformService; - private final FromJsonHelper fromJsonHelper; - private final DepositAccountPreMatureCalculationPlatformService accountPreMatureCalculationPlatformService; - private final AccountAssociationsReadPlatformService accountAssociationsReadPlatformService; - private final BulkImportWorkbookService bulkImportWorkbookService; - private final BulkImportWorkbookPopulatorService bulkImportWorkbookPopulatorService; - private final FixedDepositAccountInterestCalculationService fixedDepositAccountInterestCalculationService; - private final SqlValidator sqlValidator; - - @GET - @Path("template") - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - @Operation(summary = "Retrieve Fixed Deposit Account Template", description = """ - This is a convenience resource. It can be useful when building maintenance user interface screens for fixed deposit applications. The template data returned consists of any or all of: - - - - Field Defaults - - Allowed Value ListsExample Requests: - - - - fixeddepositaccounts/template?clientId=1""") - @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = FixedDepositAccountsApiResourceSwagger.GetFixedDepositAccountsTemplateResponse.class))) - public String template(@QueryParam("clientId") @Parameter(description = "clientId") final Long clientId, - @QueryParam("groupId") @Parameter(description = "groupId") final Long groupId, - @QueryParam("productId") @Parameter(description = "productId") final Long productId, - @DefaultValue("false") @QueryParam("staffInSelectedOfficeOnly") @Parameter(description = "staffInSelectedOfficeOnly") final boolean staffInSelectedOfficeOnly, - @Context final UriInfo uriInfo) { - - this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.FIXED_DEPOSIT_ACCOUNT_RESOURCE_NAME); - - final DepositAccountData account = this.depositAccountReadPlatformService.retrieveTemplate(DepositAccountType.FIXED_DEPOSIT, - clientId, groupId, productId, staffInSelectedOfficeOnly); - - final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); - return this.toApiJsonSerializer.serialize(settings, account, DepositsApiConstants.FIXED_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS); - } - - @GET - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - @Operation(summary = "List Fixed deposit applications/accounts", description = """ - Lists Fixed Deposit Accounts - - Example Requests: - - - - fixeddepositaccounts - - - - fixeddepositaccounts?fields=name""") - @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = FixedDepositAccountsApiResourceSwagger.GetFixedDepositAccountsResponse.class)))) - public String retrieveAll(@Context final UriInfo uriInfo, @QueryParam("paged") @Parameter(description = "paged") final Boolean paged, - @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) { - - this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.FIXED_DEPOSIT_ACCOUNT_RESOURCE_NAME); - sqlValidator.validate(orderBy); - sqlValidator.validate(sortOrder); - final PaginationParameters paginationParameters = PaginationParameters.builder().paged(Boolean.TRUE.equals(paged)).limit(limit) - .offset(offset).orderBy(orderBy).sortOrder(sortOrder).build(); - final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); - - if (paginationParameters.isPaged()) { - final Page account = this.depositAccountReadPlatformService - .retrieveAllPaged(DepositAccountType.FIXED_DEPOSIT, paginationParameters); - return this.toApiJsonSerializer.serialize(settings, account, - DepositsApiConstants.FIXED_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS); - } - - final Collection account = this.depositAccountReadPlatformService.retrieveAll(DepositAccountType.FIXED_DEPOSIT, - paginationParameters); - - return this.toApiJsonSerializer.serialize(settings, account, DepositsApiConstants.FIXED_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS); - } - - @POST - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - @Operation(summary = "Submit new fixed deposit application", description = """ - Submits a new fixed deposit application - Mandatory Fields: clientId or groupId, productId, submittedOnDate, depositAmount, depositPeriod, depositPeriodFrequencyId - - Optional Fields: accountNo, externalId, fieldOfficerId,linkAccountId(if provided initial deposit amount will be collected from this account),transferInterestToSavings(By enabling this flag all interest postings will be transferred to linked saving account )""") - @RequestBody(required = true, content = @Content(schema = @Schema(implementation = FixedDepositAccountsApiResourceSwagger.PostFixedDepositAccountsRequest.class))) - @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = FixedDepositAccountsApiResourceSwagger.PostFixedDepositAccountsResponse.class))) - public String submitApplication(@Parameter(hidden = true) final String apiRequestBodyAsJson) { - - final CommandWrapper commandRequest = new CommandWrapperBuilder().createFixedDepositAccount().withJson(apiRequestBodyAsJson) - .build(); - - final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); - - return this.toApiJsonSerializer.serialize(result); - } - - @GET - @Path("{accountId}") - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - @Operation(summary = "Retrieve a fixed deposit application/account", description = """ - Retrieves a fixed deposit application/account - - Example Requests : - - - - fixeddepositaccounts/1 - - fixeddepositaccounts/1?associations=all""") - @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = FixedDepositAccountsApiResourceSwagger.GetFixedDepositAccountsAccountIdResponse.class))) - public String retrieveOne(@PathParam("accountId") @Parameter(description = "accountId") final Long accountId, - @DefaultValue("false") @QueryParam("staffInSelectedOfficeOnly") @Parameter(description = "staffInSelectedOfficeOnly") final boolean staffInSelectedOfficeOnly, - @DefaultValue("all") @QueryParam("chargeStatus") @Parameter(description = "chargeStatus") final String chargeStatus, - @Context final UriInfo uriInfo) { - - this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.FIXED_DEPOSIT_ACCOUNT_RESOURCE_NAME); - - if (!(is(chargeStatus, "all") || is(chargeStatus, "active") || is(chargeStatus, "inactive"))) { - throw new UnrecognizedQueryParamException("status", chargeStatus, new Object[] { "all", "active", "inactive" }); - } - - final FixedDepositAccountData account = (FixedDepositAccountData) this.depositAccountReadPlatformService - .retrieveOneWithChartSlabs(DepositAccountType.FIXED_DEPOSIT, accountId); - - final Set mandatoryResponseParameters = new HashSet<>(); - final FixedDepositAccountData accountTemplate = populateTemplateAndAssociations(accountId, account, staffInSelectedOfficeOnly, - chargeStatus, uriInfo, mandatoryResponseParameters); - accountTemplate.setActivationCharge(getActivationCharge(accountId)); - final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters(), - mandatoryResponseParameters); - return this.toApiJsonSerializer.serialize(settings, accountTemplate, - DepositsApiConstants.FIXED_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS); - } - - @GET - @Path("calculate-fd-interest") - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = FixedDepositAccountsApiResourceSwagger.CalculateFixedDepositInterestResponse.class))) - public String calculateFixedDepositInterest(@Context final UriInfo uriInfo, - @QueryParam("principalAmount") @Parameter(description = "BigDecimal principalAmount") final BigDecimal principalAmount, - @QueryParam("annualInterestRate") @Parameter(description = "annualInterestRate") final BigDecimal annualInterestRate, - @QueryParam("tenureInMonths") @Parameter(description = "tenureInMonths") final Long tenureInMonths, - @QueryParam("interestCompoundingPeriodInMonths") @Parameter(description = "interestCompoundingPeriodInMonths") final Long interestCompoundingPeriodInMonths, - @QueryParam("interestPostingPeriodInMonths") @Parameter(description = "interestPostingPeriodInMonths") final Long interestPostingPeriodInMonths) { - HashMap request = new HashMap<>(); - request.put("annualInterestRate", annualInterestRate); - request.put("tenureInMonths", tenureInMonths); - request.put("interestCompoundingPeriodInMonths", interestCompoundingPeriodInMonths); - request.put("interestPostingPeriodInMonths", interestPostingPeriodInMonths); - request.put("principalAmount", principalAmount); - String apiRequestBodyAsJson = toApiJsonSerializer.serialize(request); - JsonElement jsonElement = fromJsonHelper.parse(apiRequestBodyAsJson); - HashMap result = fixedDepositAccountInterestCalculationService - .calculateInterest(new JsonQuery(apiRequestBodyAsJson, jsonElement, fromJsonHelper)); - - return toApiJsonSerializer.serializeResult(result); - - } - - private BigDecimal getActivationCharge(Long accountId) { - BigDecimal activationCharge = BigDecimal.ZERO; - Collection savingCharges = this.savingsAccountChargeReadPlatformService - .retrieveSavingsAccountCharges(accountId, "active"); - for (SavingsAccountChargeData savingsAccountChargeData : savingCharges) { - if (savingsAccountChargeData.isSavingsActivation()) { - activationCharge = activationCharge.add(savingsAccountChargeData.getAmountOutstanding()); - } - } - return activationCharge; - } - - private FixedDepositAccountData populateTemplateAndAssociations(final Long accountId, final FixedDepositAccountData savingsAccount, - final boolean staffInSelectedOfficeOnly, final String chargeStatus, final UriInfo uriInfo, - final Set mandatoryResponseParameters) { - - Collection transactions = null; - Collection charges = null; - PortfolioAccountData linkedAccount = null; - PortfolioAccountData transferToSavingsAccount = null; - - final Set associationParameters = ApiParameterHelper.extractAssociationsForResponseIfProvided(uriInfo.getQueryParameters()); - if (!associationParameters.isEmpty()) { - - if (associationParameters.contains("all")) { - associationParameters.addAll( - Arrays.asList(SavingsApiConstants.transactions, SavingsApiConstants.charges, SavingsApiConstants.linkedAccount)); - } - - if (associationParameters.contains(SavingsApiConstants.transactions)) { - mandatoryResponseParameters.add(SavingsApiConstants.transactions); - final Collection currentTransactions = this.depositAccountReadPlatformService - .retrieveAllTransactions(DepositAccountType.FIXED_DEPOSIT, accountId); - if (!CollectionUtils.isEmpty(currentTransactions)) { - transactions = currentTransactions; - } - } - - if (associationParameters.contains(SavingsApiConstants.charges)) { - mandatoryResponseParameters.add(SavingsApiConstants.charges); - final Collection currentCharges = this.savingsAccountChargeReadPlatformService - .retrieveSavingsAccountCharges(accountId, chargeStatus); - if (!CollectionUtils.isEmpty(currentCharges)) { - charges = currentCharges; - } - } - - if (associationParameters.contains(SavingsApiConstants.linkedAccount)) { - mandatoryResponseParameters.add(SavingsApiConstants.linkedAccount); - linkedAccount = this.accountAssociationsReadPlatformService.retriveSavingsLinkedAssociation(accountId); - } - } - - if (savingsAccount.getTransferToSavingsId() != null) { - transferToSavingsAccount = this.accountAssociationsReadPlatformService - .retriveSavingsAccount(savingsAccount.getTransferToSavingsId()); - } - - FixedDepositAccountData templateData = null; - final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); - - if (settings.isTemplate()) { - templateData = (FixedDepositAccountData) this.depositAccountReadPlatformService.retrieveTemplate( - DepositAccountType.FIXED_DEPOSIT, savingsAccount.getClientId(), savingsAccount.getGroupId(), - savingsAccount.getDepositProductId(), staffInSelectedOfficeOnly); - } - - return FixedDepositAccountData.associationsAndTemplate(savingsAccount, templateData, transactions, charges, linkedAccount, - transferToSavingsAccount); - } - - @PUT - @Path("{accountId}") - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - @Operation(summary = "Modify a fixed deposit application", description = "Fixed deposit application can only be modified when in 'Submitted and pending approval' state. Once the application is approved, the details cannot be changed using this method. Specific api endpoints will be created to allow change of interest detail such as rate, compounding period, posting period etc") - @RequestBody(required = true, content = @Content(schema = @Schema(implementation = FixedDepositAccountsApiResourceSwagger.PutFixedDepositAccountsAccountIdRequest.class))) - @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = FixedDepositAccountsApiResourceSwagger.PutFixedDepositAccountsAccountIdResponse.class))) - public String update(@PathParam("accountId") @Parameter(description = "accountId") final Long accountId, - @Parameter(hidden = true) final String apiRequestBodyAsJson) { - - final CommandWrapper commandRequest = new CommandWrapperBuilder().updateFixedDepositAccount(accountId) - .withJson(apiRequestBodyAsJson).build(); - - final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); - - return this.toApiJsonSerializer.serialize(result); - } - - @POST - @Path("{accountId}") - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - @Operation(summary = "Approve fixed deposit application | Undo approval fixed deposit application | Reject fixed deposit application | Withdraw fixed deposit application | Activate a fixed deposit account | Close a fixed deposit account | Premature Close a fixed deposit account | Calculate Premature amount on Fixed deposit account | Calculate Interest on Fixed Deposit Account | Post Interest on Fixed Deposit Account", description = "Approve fixed deposit application:\n\n" - + "Approves fixed deposit application so long as its in 'Submitted and pending approval' state.\n\n" - + "Undo approval fixed deposit application:\n\n" - + "Will move 'approved' fixed deposit application back to 'Submitted and pending approval' state.\n\n" - + "Reject fixed deposit application:\n\n" - + "Rejects fixed deposit application so long as its in 'Submitted and pending approval' state.\n\n" - + "Withdraw fixed deposit application:\n\n" - + "Used when an applicant withdraws from the fixed deposit application. It must be in 'Submitted and pending approval' state.\n\n" - + "Close a fixed deposit account:\n\n" - + "Results in a Matured fixed deposit account being converted into a 'closed' fixed deposit account.\n\n" - + "Premature Close a fixed deposit account:\n\n" - + "Results in an Active fixed deposit account being converted into a 'Premature Closed' fixed deposit account with options to withdraw prematured amount. (premature amount is calculated using interest rate chart applicable along with penal interest if any.)\n\n" - + "Calculate Premature amount on Fixed deposit account:\n\n" - + "Calculate premature amount on fixed deposit account till premature close date. Premature amount is calculated based on interest chart and penal interest applicable.\n\n" - + "Calculate Interest on Fixed Deposit Account:\n\n" - + "Calculates interest earned on a fixed deposit account based on todays date. It does not attempt to post or credit the interest on the account. That is responsibility of the Post Interest API that will likely be called by overnight process.\n\n" - + "Post Interest on Fixed Deposit Account:\n\n" - + "Calculates and Posts interest earned on a fixed deposit account based on today's date and whether an interest posting or crediting event is due.\n\n" - + "Showing request/response for Calculate Interest on Fixed Deposit Account") - @RequestBody(required = true, content = @Content(schema = @Schema(implementation = FixedDepositAccountsApiResourceSwagger.PostFixedDepositAccountsAccountIdRequest.class))) - @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = FixedDepositAccountsApiResourceSwagger.PostFixedDepositAccountsAccountIdResponse.class))) - public String handleCommands(@PathParam("accountId") @Parameter(description = "accountId") final Long accountId, - @QueryParam("command") @Parameter(description = "command") final String commandParam, @Context final UriInfo uriInfo, - @Parameter(hidden = true) final String apiRequestBodyAsJson) { - - String jsonApiRequest = apiRequestBodyAsJson; - if (StringUtils.isBlank(jsonApiRequest)) { - jsonApiRequest = "{}"; - } - - final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(jsonApiRequest); - - CommandProcessingResult result = null; - if (is(commandParam, "reject")) { - final CommandWrapper commandRequest = builder.rejectFixedDepositAccountApplication(accountId).build(); - result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, "withdrawnByApplicant")) { - final CommandWrapper commandRequest = builder.withdrawFixedDepositAccountApplication(accountId).build(); - result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, "approve")) { - final CommandWrapper commandRequest = builder.approveFixedDepositAccountApplication(accountId).build(); - result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, "undoapproval")) { - final CommandWrapper commandRequest = builder.undoFixedDepositAccountApplication(accountId).build(); - result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, "activate")) { - final CommandWrapper commandRequest = builder.fixedDepositAccountActivation(accountId).build(); - result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, "calculateInterest")) { - final CommandWrapper commandRequest = builder.withNoJsonBody().fixedDepositAccountInterestCalculation(accountId).build(); - result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, "postInterest")) { - final CommandWrapper commandRequest = builder.fixedDepositAccountInterestPosting(accountId).build(); - result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, "close")) { - final CommandWrapper commandRequest = builder.closeFixedDepositAccount(accountId).build(); - result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, "prematureClose")) { - final CommandWrapper commandRequest = builder.prematureCloseFixedDepositAccount(accountId).build(); - result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, "calculatePrematureAmount")) { - final JsonElement parsedQuery = this.fromJsonHelper.parse(apiRequestBodyAsJson); - final JsonQuery query = JsonQuery.from(apiRequestBodyAsJson, parsedQuery, this.fromJsonHelper); - final DepositAccountData account = this.accountPreMatureCalculationPlatformService.calculatePreMatureAmount(accountId, query, - DepositAccountType.FIXED_DEPOSIT); - final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); - return this.toApiJsonSerializer.serialize(settings, account, - DepositsApiConstants.FIXED_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS); - } - - if (result == null) { - throw new UnrecognizedQueryParamException("command", commandParam, - new Object[] { "reject", "withdrawnByApplicant", "approve", "undoapproval", "activate", "calculateInterest", - "postInterest", "close", "prematureClose", "calculatePrematureAmount" }); - } - - return this.toApiJsonSerializer.serialize(result); - } - - private boolean is(final String commandParam, final String commandValue) { - return StringUtils.isNotBlank(commandParam) && commandParam.trim().equalsIgnoreCase(commandValue); - } - - @DELETE - @Path("{accountId}") - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - @Operation(summary = "Delete a fixed deposit application", description = "At present we support hard delete of fixed deposit application so long as its in 'Submitted and pending approval' state. One the application is moves past this state, it is not possible to do a 'hard' delete of the application or the account. An API endpoint will be added to close/de-activate the fixed deposit account.") - @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = FixedDepositAccountsApiResourceSwagger.DeleteFixedDepositAccountsAccountIdResponse.class))) - public String delete(@PathParam("accountId") @Parameter(description = "accountId") final Long accountId) { - - final CommandWrapper commandRequest = new CommandWrapperBuilder().deleteFixedDepositAccount(accountId).build(); - - final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); - - return this.toApiJsonSerializer.serialize(result); - } - - @GET - @Path("{accountId}/template") - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - public String accountClosureTemplate(@PathParam("accountId") @Parameter(description = "accountId") final Long accountId, - @QueryParam("command") @Parameter(description = "command") final String commandParam, @Context final UriInfo uriInfo) { - - this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.FIXED_DEPOSIT_ACCOUNT_RESOURCE_NAME); - DepositAccountData account = null; - if (is(commandParam, "close")) { - account = this.depositAccountReadPlatformService.retrieveOneWithClosureTemplate(DepositAccountType.FIXED_DEPOSIT, accountId); - } - - final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); - return this.toApiJsonSerializer.serialize(settings, account, DepositsApiConstants.FIXED_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS); - } - - @GET - @Path("downloadtemplate") - @Produces("application/vnd.ms-excel") - public Response getFixedDepositTemplate(@QueryParam("officeId") final Long officeId, @QueryParam("staffId") final Long staffId, - @QueryParam("dateFormat") final String dateFormat) { - return bulkImportWorkbookPopulatorService.getTemplate(GlobalEntityType.FIXED_DEPOSIT_ACCOUNTS.toString(), officeId, staffId, - dateFormat); - } - - @POST - @Path("uploadtemplate") - @Consumes(MediaType.MULTIPART_FORM_DATA) - @RequestBody(description = "Upload fixed deposit template", content = { - @Content(mediaType = MediaType.MULTIPART_FORM_DATA, schema = @Schema(implementation = UploadRequest.class)) }) - public String postFixedDepositTemplate(@FormDataParam("file") InputStream uploadedInputStream, - @FormDataParam("file") FormDataContentDisposition fileDetail, @FormDataParam("locale") final String locale, - @FormDataParam("dateFormat") final String dateFormat) { - Long importDocumentId = bulkImportWorkbookService.importWorkbook(GlobalEntityType.FIXED_DEPOSIT_ACCOUNTS.toString(), - uploadedInputStream, fileDetail, locale, dateFormat); - return this.toApiJsonSerializer.serialize(importDocumentId); - } - - @GET - @Path("transaction/downloadtemplate") - @Produces("application/vnd.ms-excel") - public Response getFixedDepositTransactionTemplate(@QueryParam("officeId") final Long officeId, - @QueryParam("dateFormat") final String dateFormat) { - return bulkImportWorkbookPopulatorService.getTemplate(GlobalEntityType.FIXED_DEPOSIT_TRANSACTIONS.toString(), officeId, null, - dateFormat); - } - - @POST - @Path("transaction/uploadtemplate") - @Consumes(MediaType.MULTIPART_FORM_DATA) - @RequestBody(description = "Upload fixed deposit transaction template", content = { - @Content(mediaType = MediaType.MULTIPART_FORM_DATA, schema = @Schema(implementation = UploadRequest.class)) }) - public String postFixedDepositTransactionTemplate(@FormDataParam("file") InputStream uploadedInputStream, - @FormDataParam("file") FormDataContentDisposition fileDetail, @FormDataParam("locale") final String locale, - @FormDataParam("dateFormat") final String dateFormat) { - final Long importDocumentId = this.bulkImportWorkbookService.importWorkbook(GlobalEntityType.FIXED_DEPOSIT_TRANSACTIONS.toString(), - uploadedInputStream, fileDetail, locale, dateFormat); - return this.toApiJsonSerializer.serialize(importDocumentId); - } -} +/** + * 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.api; + +import com.google.gson.JsonElement; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; +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.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.DefaultValue; +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.Response; +import jakarta.ws.rs.core.UriInfo; +import java.io.InputStream; +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +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.bulkimport.data.GlobalEntityType; +import org.apache.fineract.infrastructure.bulkimport.service.BulkImportWorkbookPopulatorService; +import org.apache.fineract.infrastructure.bulkimport.service.BulkImportWorkbookService; +import org.apache.fineract.infrastructure.core.api.ApiParameterHelper; +import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper; +import org.apache.fineract.infrastructure.core.api.JsonQuery; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.data.PaginationParameters; +import org.apache.fineract.infrastructure.core.data.UploadRequest; +import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException; +import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings; +import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.infrastructure.core.service.Page; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.infrastructure.security.service.SqlValidator; +import org.apache.fineract.portfolio.account.data.PortfolioAccountData; +import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlatformService; +import org.apache.fineract.portfolio.savings.DepositAccountType; +import org.apache.fineract.portfolio.savings.DepositsApiConstants; +import org.apache.fineract.portfolio.savings.SavingsApiConstants; +import org.apache.fineract.portfolio.savings.data.DepositAccountData; +import org.apache.fineract.portfolio.savings.data.FixedDepositAccountData; +import org.apache.fineract.portfolio.savings.data.SavingsAccountChargeData; +import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData; +import org.apache.fineract.portfolio.savings.service.DepositAccountPreMatureCalculationPlatformService; +import org.apache.fineract.portfolio.savings.service.DepositAccountReadPlatformService; +import org.apache.fineract.portfolio.savings.service.FixedDepositAccountInterestCalculationService; +import org.apache.fineract.portfolio.savings.service.SavingsAccountChargeReadPlatformService; +import org.glassfish.jersey.media.multipart.FormDataContentDisposition; +import org.glassfish.jersey.media.multipart.FormDataParam; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; + +@Path("/v1/fixeddepositaccounts") +@Component +@Tag(name = "Fixed Deposit Account", description = "Fixed Deposit accounts are instances of a praticular fixed deposit product created. An application process around the creation of accounts is also supported.") +@RequiredArgsConstructor +public class FixedDepositAccountsApiResource { + + private final DepositAccountReadPlatformService depositAccountReadPlatformService; + private final PlatformSecurityContext context; + private final DefaultToApiJsonSerializer toApiJsonSerializer; + private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; + private final ApiRequestParameterHelper apiRequestParameterHelper; + private final SavingsAccountChargeReadPlatformService savingsAccountChargeReadPlatformService; + private final FromJsonHelper fromJsonHelper; + private final DepositAccountPreMatureCalculationPlatformService accountPreMatureCalculationPlatformService; + private final AccountAssociationsReadPlatformService accountAssociationsReadPlatformService; + private final BulkImportWorkbookService bulkImportWorkbookService; + private final BulkImportWorkbookPopulatorService bulkImportWorkbookPopulatorService; + private final FixedDepositAccountInterestCalculationService fixedDepositAccountInterestCalculationService; + private final SqlValidator sqlValidator; + + @GET + @Path("template") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Retrieve Fixed Deposit Account Template", description = """ + This is a convenience resource. It can be useful when building maintenance user interface screens for fixed deposit applications. The template data returned consists of any or all of: + + + + Field Defaults + + Allowed Value ListsExample Requests: + + + + fixeddepositaccounts/template?clientId=1""") + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = FixedDepositAccountsApiResourceSwagger.GetFixedDepositAccountsTemplateResponse.class))) + public String template(@QueryParam("clientId") @Parameter(description = "clientId") final Long clientId, + @QueryParam("groupId") @Parameter(description = "groupId") final Long groupId, + @QueryParam("productId") @Parameter(description = "productId") final Long productId, + @DefaultValue("false") @QueryParam("staffInSelectedOfficeOnly") @Parameter(description = "staffInSelectedOfficeOnly") final boolean staffInSelectedOfficeOnly, + @Context final UriInfo uriInfo) { + + this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.FIXED_DEPOSIT_ACCOUNT_RESOURCE_NAME); + + final DepositAccountData account = this.depositAccountReadPlatformService.retrieveTemplate(DepositAccountType.FIXED_DEPOSIT, + clientId, groupId, productId, staffInSelectedOfficeOnly); + + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + return this.toApiJsonSerializer.serialize(settings, account, DepositsApiConstants.FIXED_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS); + } + + @GET + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "List Fixed deposit applications/accounts", description = """ + Lists Fixed Deposit Accounts + + Example Requests: + + + + fixeddepositaccounts + + + + fixeddepositaccounts?fields=name""") + @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = FixedDepositAccountsApiResourceSwagger.GetFixedDepositAccountsResponse.class)))) + public String retrieveAll(@Context final UriInfo uriInfo, @QueryParam("paged") @Parameter(description = "paged") final Boolean paged, + @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("accountNo") @Parameter(description = "accountNo") final String accountNo, + @QueryParam("clientName") @Parameter(description = "clientName") final String clientName) { + + this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.FIXED_DEPOSIT_ACCOUNT_RESOURCE_NAME); + sqlValidator.validate(orderBy); + sqlValidator.validate(sortOrder); + final PaginationParameters paginationParameters = PaginationParameters.builder().paged(Boolean.TRUE.equals(paged)).limit(limit) + .offset(offset).orderBy(orderBy).sortOrder(sortOrder).build(); + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + + if (paginationParameters.isPaged()) { + final Page account = this.depositAccountReadPlatformService + .retrieveAllPaged(DepositAccountType.FIXED_DEPOSIT, paginationParameters, accountNo, clientName); + return this.toApiJsonSerializer.serialize(settings, account, + DepositsApiConstants.FIXED_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS); + } + + final Collection account = this.depositAccountReadPlatformService.retrieveAll(DepositAccountType.FIXED_DEPOSIT, + paginationParameters); + + return this.toApiJsonSerializer.serialize(settings, account, DepositsApiConstants.FIXED_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS); + } + + @POST + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Submit new fixed deposit application", description = """ + Submits a new fixed deposit application + Mandatory Fields: clientId or groupId, productId, submittedOnDate, depositAmount, depositPeriod, depositPeriodFrequencyId + + Optional Fields: accountNo, externalId, fieldOfficerId,linkAccountId(if provided initial deposit amount will be collected from this account),transferInterestToSavings(By enabling this flag all interest postings will be transferred to linked saving account )""") + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = FixedDepositAccountsApiResourceSwagger.PostFixedDepositAccountsRequest.class))) + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = FixedDepositAccountsApiResourceSwagger.PostFixedDepositAccountsResponse.class))) + public String submitApplication(@Parameter(hidden = true) final String apiRequestBodyAsJson) { + + final CommandWrapper commandRequest = new CommandWrapperBuilder().createFixedDepositAccount().withJson(apiRequestBodyAsJson) + .build(); + + final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + + return this.toApiJsonSerializer.serialize(result); + } + + @GET + @Path("{accountId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Retrieve a fixed deposit application/account", description = """ + Retrieves a fixed deposit application/account + + Example Requests : + + + + fixeddepositaccounts/1 + + fixeddepositaccounts/1?associations=all""") + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = FixedDepositAccountsApiResourceSwagger.GetFixedDepositAccountsAccountIdResponse.class))) + public String retrieveOne(@PathParam("accountId") @Parameter(description = "accountId") final Long accountId, + @DefaultValue("false") @QueryParam("staffInSelectedOfficeOnly") @Parameter(description = "staffInSelectedOfficeOnly") final boolean staffInSelectedOfficeOnly, + @DefaultValue("all") @QueryParam("chargeStatus") @Parameter(description = "chargeStatus") final String chargeStatus, + @Context final UriInfo uriInfo) { + + this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.FIXED_DEPOSIT_ACCOUNT_RESOURCE_NAME); + + if (!(is(chargeStatus, "all") || is(chargeStatus, "active") || is(chargeStatus, "inactive"))) { + throw new UnrecognizedQueryParamException("status", chargeStatus, new Object[] { "all", "active", "inactive" }); + } + + final FixedDepositAccountData account = (FixedDepositAccountData) this.depositAccountReadPlatformService + .retrieveOneWithChartSlabs(DepositAccountType.FIXED_DEPOSIT, accountId); + + final Set mandatoryResponseParameters = new HashSet<>(); + final FixedDepositAccountData accountTemplate = populateTemplateAndAssociations(accountId, account, staffInSelectedOfficeOnly, + chargeStatus, uriInfo, mandatoryResponseParameters); + accountTemplate.setActivationCharge(getActivationCharge(accountId)); + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters(), + mandatoryResponseParameters); + return this.toApiJsonSerializer.serialize(settings, accountTemplate, + DepositsApiConstants.FIXED_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS); + } + + @GET + @Path("calculate-fd-interest") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = FixedDepositAccountsApiResourceSwagger.CalculateFixedDepositInterestResponse.class))) + public String calculateFixedDepositInterest(@Context final UriInfo uriInfo, + @QueryParam("principalAmount") @Parameter(description = "BigDecimal principalAmount") final BigDecimal principalAmount, + @QueryParam("annualInterestRate") @Parameter(description = "annualInterestRate") final BigDecimal annualInterestRate, + @QueryParam("tenureInMonths") @Parameter(description = "tenureInMonths") final Long tenureInMonths, + @QueryParam("interestCompoundingPeriodInMonths") @Parameter(description = "interestCompoundingPeriodInMonths") final Long interestCompoundingPeriodInMonths, + @QueryParam("interestPostingPeriodInMonths") @Parameter(description = "interestPostingPeriodInMonths") final Long interestPostingPeriodInMonths) { + HashMap request = new HashMap<>(); + request.put("annualInterestRate", annualInterestRate); + request.put("tenureInMonths", tenureInMonths); + request.put("interestCompoundingPeriodInMonths", interestCompoundingPeriodInMonths); + request.put("interestPostingPeriodInMonths", interestPostingPeriodInMonths); + request.put("principalAmount", principalAmount); + String apiRequestBodyAsJson = toApiJsonSerializer.serialize(request); + JsonElement jsonElement = fromJsonHelper.parse(apiRequestBodyAsJson); + HashMap result = fixedDepositAccountInterestCalculationService + .calculateInterest(new JsonQuery(apiRequestBodyAsJson, jsonElement, fromJsonHelper)); + + return toApiJsonSerializer.serializeResult(result); + + } + + private BigDecimal getActivationCharge(Long accountId) { + BigDecimal activationCharge = BigDecimal.ZERO; + Collection savingCharges = this.savingsAccountChargeReadPlatformService + .retrieveSavingsAccountCharges(accountId, "active"); + for (SavingsAccountChargeData savingsAccountChargeData : savingCharges) { + if (savingsAccountChargeData.isSavingsActivation()) { + activationCharge = activationCharge.add(savingsAccountChargeData.getAmountOutstanding()); + } + } + return activationCharge; + } + + private FixedDepositAccountData populateTemplateAndAssociations(final Long accountId, final FixedDepositAccountData savingsAccount, + final boolean staffInSelectedOfficeOnly, final String chargeStatus, final UriInfo uriInfo, + final Set mandatoryResponseParameters) { + + Collection transactions = null; + Collection charges = null; + PortfolioAccountData linkedAccount = null; + PortfolioAccountData transferToSavingsAccount = null; + + final Set associationParameters = ApiParameterHelper.extractAssociationsForResponseIfProvided(uriInfo.getQueryParameters()); + if (!associationParameters.isEmpty()) { + + if (associationParameters.contains("all")) { + associationParameters.addAll( + Arrays.asList(SavingsApiConstants.transactions, SavingsApiConstants.charges, SavingsApiConstants.linkedAccount)); + } + + if (associationParameters.contains(SavingsApiConstants.transactions)) { + mandatoryResponseParameters.add(SavingsApiConstants.transactions); + final Collection currentTransactions = this.depositAccountReadPlatformService + .retrieveAllTransactions(DepositAccountType.FIXED_DEPOSIT, accountId); + if (!CollectionUtils.isEmpty(currentTransactions)) { + transactions = currentTransactions; + } + } + + if (associationParameters.contains(SavingsApiConstants.charges)) { + mandatoryResponseParameters.add(SavingsApiConstants.charges); + final Collection currentCharges = this.savingsAccountChargeReadPlatformService + .retrieveSavingsAccountCharges(accountId, chargeStatus); + if (!CollectionUtils.isEmpty(currentCharges)) { + charges = currentCharges; + } + } + + if (associationParameters.contains(SavingsApiConstants.linkedAccount)) { + mandatoryResponseParameters.add(SavingsApiConstants.linkedAccount); + linkedAccount = this.accountAssociationsReadPlatformService.retriveSavingsLinkedAssociation(accountId); + } + } + + if (savingsAccount.getTransferToSavingsId() != null) { + transferToSavingsAccount = this.accountAssociationsReadPlatformService + .retriveSavingsAccount(savingsAccount.getTransferToSavingsId()); + } + + FixedDepositAccountData templateData = null; + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + + if (settings.isTemplate()) { + templateData = (FixedDepositAccountData) this.depositAccountReadPlatformService.retrieveTemplate( + DepositAccountType.FIXED_DEPOSIT, savingsAccount.getClientId(), savingsAccount.getGroupId(), + savingsAccount.getDepositProductId(), staffInSelectedOfficeOnly); + } + + return FixedDepositAccountData.associationsAndTemplate(savingsAccount, templateData, transactions, charges, linkedAccount, + transferToSavingsAccount); + } + + @PUT + @Path("{accountId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Modify a fixed deposit application", description = "Fixed deposit application can only be modified when in 'Submitted and pending approval' state. Once the application is approved, the details cannot be changed using this method. Specific api endpoints will be created to allow change of interest detail such as rate, compounding period, posting period etc") + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = FixedDepositAccountsApiResourceSwagger.PutFixedDepositAccountsAccountIdRequest.class))) + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = FixedDepositAccountsApiResourceSwagger.PutFixedDepositAccountsAccountIdResponse.class))) + public String update(@PathParam("accountId") @Parameter(description = "accountId") final Long accountId, + @Parameter(hidden = true) final String apiRequestBodyAsJson) { + + final CommandWrapper commandRequest = new CommandWrapperBuilder().updateFixedDepositAccount(accountId) + .withJson(apiRequestBodyAsJson).build(); + + final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + + return this.toApiJsonSerializer.serialize(result); + } + + @POST + @Path("{accountId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Approve fixed deposit application | Undo approval fixed deposit application | Reject fixed deposit application | Withdraw fixed deposit application | Activate a fixed deposit account | Close a fixed deposit account | Premature Close a fixed deposit account | Calculate Premature amount on Fixed deposit account | Calculate Interest on Fixed Deposit Account | Post Interest on Fixed Deposit Account", description = "Approve fixed deposit application:\n\n" + + "Approves fixed deposit application so long as its in 'Submitted and pending approval' state.\n\n" + + "Undo approval fixed deposit application:\n\n" + + "Will move 'approved' fixed deposit application back to 'Submitted and pending approval' state.\n\n" + + "Reject fixed deposit application:\n\n" + + "Rejects fixed deposit application so long as its in 'Submitted and pending approval' state.\n\n" + + "Withdraw fixed deposit application:\n\n" + + "Used when an applicant withdraws from the fixed deposit application. It must be in 'Submitted and pending approval' state.\n\n" + + "Close a fixed deposit account:\n\n" + + "Results in a Matured fixed deposit account being converted into a 'closed' fixed deposit account.\n\n" + + "Premature Close a fixed deposit account:\n\n" + + "Results in an Active fixed deposit account being converted into a 'Premature Closed' fixed deposit account with options to withdraw prematured amount. (premature amount is calculated using interest rate chart applicable along with penal interest if any.)\n\n" + + "Calculate Premature amount on Fixed deposit account:\n\n" + + "Calculate premature amount on fixed deposit account till premature close date. Premature amount is calculated based on interest chart and penal interest applicable.\n\n" + + "Calculate Interest on Fixed Deposit Account:\n\n" + + "Calculates interest earned on a fixed deposit account based on todays date. It does not attempt to post or credit the interest on the account. That is responsibility of the Post Interest API that will likely be called by overnight process.\n\n" + + "Post Interest on Fixed Deposit Account:\n\n" + + "Calculates and Posts interest earned on a fixed deposit account based on today's date and whether an interest posting or crediting event is due.\n\n" + + "Showing request/response for Calculate Interest on Fixed Deposit Account") + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = FixedDepositAccountsApiResourceSwagger.PostFixedDepositAccountsAccountIdRequest.class))) + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = FixedDepositAccountsApiResourceSwagger.PostFixedDepositAccountsAccountIdResponse.class))) + public String handleCommands(@PathParam("accountId") @Parameter(description = "accountId") final Long accountId, + @QueryParam("command") @Parameter(description = "command") final String commandParam, @Context final UriInfo uriInfo, + @Parameter(hidden = true) final String apiRequestBodyAsJson) { + + String jsonApiRequest = apiRequestBodyAsJson; + if (StringUtils.isBlank(jsonApiRequest)) { + jsonApiRequest = "{}"; + } + + final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(jsonApiRequest); + + CommandProcessingResult result = null; + if (is(commandParam, "reject")) { + final CommandWrapper commandRequest = builder.rejectFixedDepositAccountApplication(accountId).build(); + result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (is(commandParam, "withdrawnByApplicant")) { + final CommandWrapper commandRequest = builder.withdrawFixedDepositAccountApplication(accountId).build(); + result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (is(commandParam, "approve")) { + final CommandWrapper commandRequest = builder.approveFixedDepositAccountApplication(accountId).build(); + result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (is(commandParam, "undoapproval")) { + final CommandWrapper commandRequest = builder.undoFixedDepositAccountApplication(accountId).build(); + result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (is(commandParam, "activate")) { + final CommandWrapper commandRequest = builder.fixedDepositAccountActivation(accountId).build(); + result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (is(commandParam, "calculateInterest")) { + final CommandWrapper commandRequest = builder.withNoJsonBody().fixedDepositAccountInterestCalculation(accountId).build(); + result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (is(commandParam, "postInterest")) { + final CommandWrapper commandRequest = builder.fixedDepositAccountInterestPosting(accountId).build(); + result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (is(commandParam, "close")) { + final CommandWrapper commandRequest = builder.closeFixedDepositAccount(accountId).build(); + result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (is(commandParam, "prematureClose")) { + final CommandWrapper commandRequest = builder.prematureCloseFixedDepositAccount(accountId).build(); + result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (is(commandParam, "calculatePrematureAmount")) { + final JsonElement parsedQuery = this.fromJsonHelper.parse(apiRequestBodyAsJson); + final JsonQuery query = JsonQuery.from(apiRequestBodyAsJson, parsedQuery, this.fromJsonHelper); + final DepositAccountData account = this.accountPreMatureCalculationPlatformService.calculatePreMatureAmount(accountId, query, + DepositAccountType.FIXED_DEPOSIT); + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + return this.toApiJsonSerializer.serialize(settings, account, + DepositsApiConstants.FIXED_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS); + } + + if (result == null) { + throw new UnrecognizedQueryParamException("command", commandParam, + new Object[] { "reject", "withdrawnByApplicant", "approve", "undoapproval", "activate", "calculateInterest", + "postInterest", "close", "prematureClose", "calculatePrematureAmount" }); + } + + return this.toApiJsonSerializer.serialize(result); + } + + private boolean is(final String commandParam, final String commandValue) { + return StringUtils.isNotBlank(commandParam) && commandParam.trim().equalsIgnoreCase(commandValue); + } + + @DELETE + @Path("{accountId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Delete a fixed deposit application", description = "At present we support hard delete of fixed deposit application so long as its in 'Submitted and pending approval' state. One the application is moves past this state, it is not possible to do a 'hard' delete of the application or the account. An API endpoint will be added to close/de-activate the fixed deposit account.") + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = FixedDepositAccountsApiResourceSwagger.DeleteFixedDepositAccountsAccountIdResponse.class))) + public String delete(@PathParam("accountId") @Parameter(description = "accountId") final Long accountId) { + + final CommandWrapper commandRequest = new CommandWrapperBuilder().deleteFixedDepositAccount(accountId).build(); + + final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + + return this.toApiJsonSerializer.serialize(result); + } + + @GET + @Path("{accountId}/template") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String accountClosureTemplate(@PathParam("accountId") @Parameter(description = "accountId") final Long accountId, + @QueryParam("command") @Parameter(description = "command") final String commandParam, @Context final UriInfo uriInfo) { + + this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.FIXED_DEPOSIT_ACCOUNT_RESOURCE_NAME); + DepositAccountData account = null; + if (is(commandParam, "close")) { + account = this.depositAccountReadPlatformService.retrieveOneWithClosureTemplate(DepositAccountType.FIXED_DEPOSIT, accountId); + } + + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + return this.toApiJsonSerializer.serialize(settings, account, DepositsApiConstants.FIXED_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS); + } + + @GET + @Path("downloadtemplate") + @Produces("application/vnd.ms-excel") + public Response getFixedDepositTemplate(@QueryParam("officeId") final Long officeId, @QueryParam("staffId") final Long staffId, + @QueryParam("dateFormat") final String dateFormat) { + return bulkImportWorkbookPopulatorService.getTemplate(GlobalEntityType.FIXED_DEPOSIT_ACCOUNTS.toString(), officeId, staffId, + dateFormat); + } + + @POST + @Path("uploadtemplate") + @Consumes(MediaType.MULTIPART_FORM_DATA) + @RequestBody(description = "Upload fixed deposit template", content = { + @Content(mediaType = MediaType.MULTIPART_FORM_DATA, schema = @Schema(implementation = UploadRequest.class)) }) + public String postFixedDepositTemplate(@FormDataParam("file") InputStream uploadedInputStream, + @FormDataParam("file") FormDataContentDisposition fileDetail, @FormDataParam("locale") final String locale, + @FormDataParam("dateFormat") final String dateFormat) { + Long importDocumentId = bulkImportWorkbookService.importWorkbook(GlobalEntityType.FIXED_DEPOSIT_ACCOUNTS.toString(), + uploadedInputStream, fileDetail, locale, dateFormat); + return this.toApiJsonSerializer.serialize(importDocumentId); + } + + @GET + @Path("transaction/downloadtemplate") + @Produces("application/vnd.ms-excel") + public Response getFixedDepositTransactionTemplate(@QueryParam("officeId") final Long officeId, + @QueryParam("dateFormat") final String dateFormat) { + return bulkImportWorkbookPopulatorService.getTemplate(GlobalEntityType.FIXED_DEPOSIT_TRANSACTIONS.toString(), officeId, null, + dateFormat); + } + + @POST + @Path("transaction/uploadtemplate") + @Consumes(MediaType.MULTIPART_FORM_DATA) + @RequestBody(description = "Upload fixed deposit transaction template", content = { + @Content(mediaType = MediaType.MULTIPART_FORM_DATA, schema = @Schema(implementation = UploadRequest.class)) }) + public String postFixedDepositTransactionTemplate(@FormDataParam("file") InputStream uploadedInputStream, + @FormDataParam("file") FormDataContentDisposition fileDetail, @FormDataParam("locale") final String locale, + @FormDataParam("dateFormat") final String dateFormat) { + final Long importDocumentId = this.bulkImportWorkbookService.importWorkbook(GlobalEntityType.FIXED_DEPOSIT_TRANSACTIONS.toString(), + uploadedInputStream, fileDetail, locale, dateFormat); + return this.toApiJsonSerializer.serialize(importDocumentId); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/RecurringDepositAccountsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/RecurringDepositAccountsApiResource.java index 5027d4b3b..0ed66a8ff 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/RecurringDepositAccountsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/RecurringDepositAccountsApiResource.java @@ -1,470 +1,472 @@ -/** - * 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.api; - -import com.google.gson.JsonElement; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.ArraySchema; -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.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.DELETE; -import jakarta.ws.rs.DefaultValue; -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.Response; -import jakarta.ws.rs.core.UriInfo; -import java.io.InputStream; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Set; -import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.StringUtils; -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.bulkimport.data.GlobalEntityType; -import org.apache.fineract.infrastructure.bulkimport.service.BulkImportWorkbookPopulatorService; -import org.apache.fineract.infrastructure.bulkimport.service.BulkImportWorkbookService; -import org.apache.fineract.infrastructure.core.api.ApiParameterHelper; -import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper; -import org.apache.fineract.infrastructure.core.api.JsonQuery; -import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; -import org.apache.fineract.infrastructure.core.data.PaginationParameters; -import org.apache.fineract.infrastructure.core.data.UploadRequest; -import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException; -import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings; -import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; -import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; -import org.apache.fineract.infrastructure.core.service.Page; -import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; -import org.apache.fineract.infrastructure.security.service.SqlValidator; -import org.apache.fineract.portfolio.savings.DepositAccountType; -import org.apache.fineract.portfolio.savings.DepositsApiConstants; -import org.apache.fineract.portfolio.savings.SavingsApiConstants; -import org.apache.fineract.portfolio.savings.data.DepositAccountData; -import org.apache.fineract.portfolio.savings.data.RecurringDepositAccountData; -import org.apache.fineract.portfolio.savings.data.SavingsAccountChargeData; -import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData; -import org.apache.fineract.portfolio.savings.service.DepositAccountPreMatureCalculationPlatformService; -import org.apache.fineract.portfolio.savings.service.DepositAccountReadPlatformService; -import org.apache.fineract.portfolio.savings.service.RecurringDepositAccountInterestCalculationService; -import org.apache.fineract.portfolio.savings.service.SavingsAccountChargeReadPlatformService; -import org.glassfish.jersey.media.multipart.FormDataContentDisposition; -import org.glassfish.jersey.media.multipart.FormDataParam; -import org.springframework.stereotype.Component; -import org.springframework.util.CollectionUtils; - -@Path("/v1/recurringdepositaccounts") -@Component -@Tag(name = "Recurring Deposit Account", description = "Recurring Deposit accounts are instances of a praticular recurring deposit product created. An application process around the creation of accounts is also supported.") -@RequiredArgsConstructor -public class RecurringDepositAccountsApiResource { - - private final DepositAccountReadPlatformService depositAccountReadPlatformService; - private final PlatformSecurityContext context; - private final DefaultToApiJsonSerializer toApiJsonSerializer; - private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; - private final ApiRequestParameterHelper apiRequestParameterHelper; - private final SavingsAccountChargeReadPlatformService savingsAccountChargeReadPlatformService; - private final FromJsonHelper fromJsonHelper; - private final DepositAccountPreMatureCalculationPlatformService accountPreMatureCalculationPlatformService; - private final BulkImportWorkbookService bulkImportWorkbookService; - private final BulkImportWorkbookPopulatorService bulkImportWorkbookPopulatorService; - private final RecurringDepositAccountInterestCalculationService recurringDepositAccountInterestCalculationService; - private final SqlValidator sqlValidator; - - @GET - @Path("template") - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - @Operation(summary = "Retrieve recurring Deposit Account Template", description = "This is a convenience resource. It can be useful when building maintenance user interface screens for recurring deposit applications. The template data returned consists of any or all of:\n" - + "\n" + "Field Defaults\n" + "Allowed Value Lists\n\n" + "Example Requests:\n" + "\n" - + "recurringdepositaccounts/template?clientId=1\n" + "\n" + "\n" + "recurringdepositaccounts/template?clientId=1&productId=1") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = RecurringDepositAccountsApiResourceSwagger.GetRecurringDepositAccountsTemplateResponse.class))) }) - public String template(@QueryParam("clientId") @Parameter(description = "clientId") final Long clientId, - @QueryParam("groupId") @Parameter(description = "groupId") final Long groupId, - @QueryParam("productId") @Parameter(description = "productId") final Long productId, - @DefaultValue("false") @QueryParam("staffInSelectedOfficeOnly") @Parameter(description = "staffInSelectedOfficeOnly") final boolean staffInSelectedOfficeOnly, - @Context final UriInfo uriInfo) { - - this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME); - - final DepositAccountData account = this.depositAccountReadPlatformService.retrieveTemplate(DepositAccountType.RECURRING_DEPOSIT, - clientId, groupId, productId, staffInSelectedOfficeOnly); - - final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); - return this.toApiJsonSerializer.serialize(settings, account, - DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS); - } - - @GET - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - @Operation(summary = "List Recurring deposit applications/accounts", description = "Lists Recurring deposit applications/accounts\n\n" - + "Example Requests:\n" + "\n" + "recurringdepositaccounts\n" + "\n" + "\n" + "recurringdepositaccounts?fields=name") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = RecurringDepositAccountsApiResourceSwagger.GetRecurringDepositAccountsResponse.class)))) }) - public String retrieveAll(@Context final UriInfo uriInfo, @QueryParam("paged") @Parameter(description = "paged") final Boolean paged, - @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) { - - this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME); - sqlValidator.validate(orderBy); - sqlValidator.validate(sortOrder); - final PaginationParameters paginationParameters = PaginationParameters.builder().paged(Boolean.TRUE.equals(paged)).limit(limit) - .offset(offset).orderBy(orderBy).sortOrder(sortOrder).build(); - final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); - - if (paginationParameters.isPaged()) { - final Page account = this.depositAccountReadPlatformService - .retrieveAllPaged(DepositAccountType.RECURRING_DEPOSIT, paginationParameters); - return this.toApiJsonSerializer.serialize(settings, account, - DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS); - } - - final Collection account = this.depositAccountReadPlatformService - .retrieveAll(DepositAccountType.RECURRING_DEPOSIT, paginationParameters); - - return this.toApiJsonSerializer.serialize(settings, account, - DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS); - } - - @POST - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - @Operation(summary = "Submit new recurring deposit application", description = "Submits new recurring deposit application\n\n" - + "Mandatory Fields: clientId or groupId, productId, submittedOnDate, depositAmount, depositPeriod, depositPeriodFrequencyId\n\n" - + "Optional Fields: accountNo, externalId, fieldOfficerId,linkAccountId(if provided initial deposit amount will be collected from this account),transferInterestToSavings(By enabling this flag all interest postings will be transferred to linked saving account )\n\n" - + "Inherited from Product (if not provided): interestCompoundingPeriodType, interestCalculationType, interestCalculationDaysInYearType, lockinPeriodFrequency, lockinPeriodFrequencyType, preClosurePenalApplicable, preClosurePenalInterest, preClosurePenalInterestOnTypeId, charts, withHoldTax") - @RequestBody(required = true, content = @Content(schema = @Schema(implementation = RecurringDepositAccountsApiResourceSwagger.PostRecurringDepositAccountsRequest.class))) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = RecurringDepositAccountsApiResourceSwagger.PostRecurringDepositAccountsResponse.class))) }) - public String submitApplication(@Parameter(hidden = true) final String apiRequestBodyAsJson) { - - final CommandWrapper commandRequest = new CommandWrapperBuilder().createRecurringDepositAccount().withJson(apiRequestBodyAsJson) - .build(); - - final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); - - return this.toApiJsonSerializer.serialize(result); - } - - @GET - @Path("{accountId}") - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - @Operation(summary = "Retrieve a recurring deposit application/account", description = "Retrieves a recurring deposit application/account\n\n" - + "Example Requests :\n" + "\n" + "recurringdepositaccounts/1\n" + "\n" + "\n" + "recurringdepositaccounts/1?associations=all") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = RecurringDepositAccountsApiResourceSwagger.GetRecurringDepositAccountsAccountIdResponse.class))) }) - public String retrieveOne(@PathParam("accountId") @Parameter(description = "accountId") final Long accountId, - @DefaultValue("false") @QueryParam("staffInSelectedOfficeOnly") @Parameter(description = "staffInSelectedOfficeOnly") final boolean staffInSelectedOfficeOnly, - @DefaultValue("all") @QueryParam("chargeStatus") @Parameter(description = "chargeStatus") final String chargeStatus, - @Context final UriInfo uriInfo) { - - this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME); - - if (!(is(chargeStatus, "all") || is(chargeStatus, "active") || is(chargeStatus, "inactive"))) { - throw new UnrecognizedQueryParamException("status", chargeStatus, new Object[] { "all", "active", "inactive" }); - } - - final RecurringDepositAccountData account = (RecurringDepositAccountData) this.depositAccountReadPlatformService - .retrieveOneWithChartSlabs(DepositAccountType.RECURRING_DEPOSIT, accountId); - - final Set mandatoryResponseParameters = new HashSet<>(); - final RecurringDepositAccountData accountTemplate = populateTemplateAndAssociations(accountId, account, staffInSelectedOfficeOnly, - chargeStatus, uriInfo, mandatoryResponseParameters); - - final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters(), - mandatoryResponseParameters); - return this.toApiJsonSerializer.serialize(settings, accountTemplate, - DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS); - } - - private RecurringDepositAccountData populateTemplateAndAssociations(final Long accountId, - final RecurringDepositAccountData savingsAccount, final boolean staffInSelectedOfficeOnly, final String chargeStatus, - final UriInfo uriInfo, final Set mandatoryResponseParameters) { - - Collection transactions = null; - Collection charges = null; - - final Set associationParameters = ApiParameterHelper.extractAssociationsForResponseIfProvided(uriInfo.getQueryParameters()); - if (!associationParameters.isEmpty()) { - - if (associationParameters.contains("all")) { - associationParameters.addAll(Arrays.asList(SavingsApiConstants.transactions, SavingsApiConstants.charges)); - } - - if (associationParameters.contains(SavingsApiConstants.transactions)) { - mandatoryResponseParameters.add(SavingsApiConstants.transactions); - final Collection currentTransactions = this.depositAccountReadPlatformService - .retrieveAllTransactions(DepositAccountType.RECURRING_DEPOSIT, accountId); - if (!CollectionUtils.isEmpty(currentTransactions)) { - transactions = currentTransactions; - } - } - - if (associationParameters.contains(SavingsApiConstants.charges)) { - mandatoryResponseParameters.add(SavingsApiConstants.charges); - final Collection currentCharges = this.savingsAccountChargeReadPlatformService - .retrieveSavingsAccountCharges(accountId, chargeStatus); - if (!CollectionUtils.isEmpty(currentCharges)) { - charges = currentCharges; - } - } - } - - RecurringDepositAccountData templateData = null; - final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); - - if (settings.isTemplate()) { - templateData = (RecurringDepositAccountData) this.depositAccountReadPlatformService.retrieveTemplate( - DepositAccountType.RECURRING_DEPOSIT, savingsAccount.getClientId(), savingsAccount.getGroupId(), - savingsAccount.getDepositProductId(), staffInSelectedOfficeOnly); - } - - return RecurringDepositAccountData.withTemplateOptions(savingsAccount, templateData, transactions, charges); - } - - @PUT - @Path("{accountId}") - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - @Operation(summary = "Modify a recurring deposit application", description = "Recurring deposit application can only be modified when in 'Submitted and pending approval' state. Once the application is approved, the details cannot be changed using this method. Specific api endpoints will be created to allow change of interest detail such as rate, compounding period, posting period etc") - @RequestBody(required = true, content = @Content(schema = @Schema(implementation = RecurringDepositAccountsApiResourceSwagger.PutRecurringDepositAccountsAccountIdRequest.class))) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = RecurringDepositAccountsApiResourceSwagger.PutRecurringDepositAccountsAccountIdResponse.class))) }) - public String update(@PathParam("accountId") @Parameter(description = "accountId") final Long accountId, - @Parameter(hidden = true) final String apiRequestBodyAsJson) { - - final CommandWrapper commandRequest = new CommandWrapperBuilder().updateRecurringDepositAccount(accountId) - .withJson(apiRequestBodyAsJson).build(); - - final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); - - return this.toApiJsonSerializer.serialize(result); - } - - @POST - @Path("{accountId}") - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - @Operation(summary = "Approve recurring deposit application | Undo approval recurring deposit application | Reject recurring deposit application | Withdraw recurring deposit application | Activate a recurring deposit account | Update the recommended deposit amount for a recurring deposit account | Close a recurring deposit account | Premature Close a recurring deposit account | Calculate Premature amount on Recurring deposit account | Calculate Interest on recurring Deposit Account | Post Interest on recurring Deposit Account", description = "Approve recurring deposit application:\n\n" - + "Approves recurring deposit application so long as its in 'Submitted and pending approval' state.\n\n" - + "Undo approval recurring deposit application:\n\n" - + "Will move 'approved' recurring deposit application back to 'Submitted and pending approval' state.\n\n" - + "Reject recurring deposit application\n\n" - + "Rejects recurring deposit application so long as its in 'Submitted and pending approval' state.\n\n" - + "Withdraw recurring deposit application:\n\n" - + "Used when an applicant withdraws from the recurring deposit application. It must be in 'Submitted and pending approval' state.\n\n" - + "Activate a recurring deposit account:\n\n" - + "Results in an approved recurring deposit application being converted into an 'active' recurring deposit account.\n\n" - + "Update the recommended deposit amount for a recurring deposit account:\n\n" - + "Updates the recommended deposit amount for a RD account as on the effective date.\n\n" - + "Close a recurring deposit account\n\n" - + "Results in a Matured recurring deposit account being converted into a 'closed' recurring deposit account.\n" + "\n" - + "On account close allowed actions are." + "Premature Close a recurring deposit account:\n\n" - + "Results in an Active recurring deposit account being converted into a 'Premature Closed' recurring deposit account with options to withdraw prematured amount. (premature amount is calculated using interest rate chart applicable along with penal interest if any.)\n" - + "\n" + "On account premature closure allowed actions are.\n\n" - + "Calculate Premature amount on Recurring deposit account:\n\n" - + "Calculate premature amount on recurring deposit till premature close date. Premature amount is calculated based on interest chart and penal interest applicable if any.\n\n" - + "Calculate Interest on recurring Deposit Account:\n\n" - + "Calculates interest earned on a recurring deposit account based on todays date. It does not attempt to post or credit the interest on the account. That is responsibility of the Post Interest API that will likely be called by overnight process.\n\n" - + "Post Interest on recurring Deposit Account:\n\n" - + "Calculates and Posts interest earned on a recurring deposit account based on todays date and whether an interest posting or crediting event is due.\n\n" - + "Showing request/response for 'Post Interest on recurring Deposit Account'") - @RequestBody(required = true, content = @Content(schema = @Schema(implementation = RecurringDepositAccountsApiResourceSwagger.PostRecurringDepositAccountsAccountIdRequest.class))) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = RecurringDepositAccountsApiResourceSwagger.PostRecurringDepositAccountsAccountIdResponse.class))) }) - public String handleCommands(@PathParam("accountId") @Parameter(description = "accountId") final Long accountId, - @QueryParam("command") @Parameter(description = "command") final String commandParam, @Context final UriInfo uriInfo, - @Parameter(hidden = true) final String apiRequestBodyAsJson) { - - String jsonApiRequest = apiRequestBodyAsJson; - if (StringUtils.isBlank(jsonApiRequest)) { - jsonApiRequest = "{}"; - } - - final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(jsonApiRequest); - - CommandProcessingResult result = null; - if (is(commandParam, "reject")) { - final CommandWrapper commandRequest = builder.rejectRecurringDepositAccountApplication(accountId).build(); - result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, "withdrawnByApplicant")) { - final CommandWrapper commandRequest = builder.withdrawRecurringDepositAccountApplication(accountId).build(); - result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, "approve")) { - final CommandWrapper commandRequest = builder.approveRecurringDepositAccountApplication(accountId).build(); - result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, "undoapproval")) { - final CommandWrapper commandRequest = builder.undoRecurringDepositAccountApplication(accountId).build(); - result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, "activate")) { - final CommandWrapper commandRequest = builder.recurringDepositAccountActivation(accountId).build(); - result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, "calculateInterest")) { - final CommandWrapper commandRequest = builder.withNoJsonBody().recurringDepositAccountInterestCalculation(accountId).build(); - result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, DepositsApiConstants.UPDATE_DEPOSIT_AMOUNT)) { - final CommandWrapper commandRequest = builder.updateDepositAmountForRecurringDepositAccount(accountId) - .withJson(apiRequestBodyAsJson).build(); - result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, "postInterest")) { - final CommandWrapper commandRequest = builder.recurringDepositAccountInterestPosting(accountId).build(); - result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, "close")) { - final CommandWrapper commandRequest = builder.closeRecurringDepositAccount(accountId).build(); - result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, "prematureClose")) { - final CommandWrapper commandRequest = builder.prematureCloseRecurringDepositAccount(accountId).build(); - result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, "calculatePrematureAmount")) { - final JsonElement parsedQuery = this.fromJsonHelper.parse(apiRequestBodyAsJson); - final JsonQuery query = JsonQuery.from(apiRequestBodyAsJson, parsedQuery, this.fromJsonHelper); - final DepositAccountData account = this.accountPreMatureCalculationPlatformService.calculatePreMatureAmount(accountId, query, - DepositAccountType.RECURRING_DEPOSIT); - final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); - return this.toApiJsonSerializer.serialize(settings, account, - DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS); - } - - if (result == null) { - throw new UnrecognizedQueryParamException("command", commandParam, - new Object[] { "reject", "withdrawnByApplicant", "approve", "undoapproval", "activate", "calculateInterest", - "postInterest", "close", "prematureClose", "calculatePrematureAmount" }); - } - - return this.toApiJsonSerializer.serialize(result); - } - - @POST - @Path("calculate-rd-interest") - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - public String calculateRecurringDepositInterest(@Context final UriInfo uriInfo, final String apiRequestBodyAsJson) { - JsonElement jsonElement = fromJsonHelper.parse(apiRequestBodyAsJson); - HashMap result = recurringDepositAccountInterestCalculationService - .calculateInterest(new JsonQuery(apiRequestBodyAsJson, jsonElement, fromJsonHelper)); - - return toApiJsonSerializer.serializeResult(result); - } - - private boolean is(final String commandParam, final String commandValue) { - return StringUtils.isNotBlank(commandParam) && commandParam.trim().equalsIgnoreCase(commandValue); - } - - @DELETE - @Path("{accountId}") - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - @Operation(summary = "Delete a recurring deposit application", description = "At present we support hard delete of recurring deposit application so long as its in 'Submitted and pending approval' state. One the application is moves past this state, it is not possible to do a 'hard' delete of the application or the account. An API endpoint will be added to close/de-activate the recurring deposit account.") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = RecurringDepositAccountsApiResourceSwagger.DeleteRecurringDepositAccountsResponse.class))) }) - public String delete(@PathParam("accountId") @Parameter(description = "accountId") final Long accountId) { - - final CommandWrapper commandRequest = new CommandWrapperBuilder().deleteRecurringDepositAccount(accountId).build(); - - final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); - - return this.toApiJsonSerializer.serialize(result); - } - - @GET - @Path("{accountId}/template") - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - public String accountClosureTemplate(@PathParam("accountId") @Parameter(description = "accountId") final Long accountId, - @QueryParam("command") @Parameter(description = "command") final String commandParam, @Context final UriInfo uriInfo) { - - this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME); - DepositAccountData account = null; - if (is(commandParam, "close")) { - account = this.depositAccountReadPlatformService.retrieveOneWithClosureTemplate(DepositAccountType.RECURRING_DEPOSIT, - accountId); - - } - - final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); - return this.toApiJsonSerializer.serialize(settings, account, - DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS); - } - - @GET - @Path("downloadtemplate") - @Produces("application/vnd.ms-excel") - public Response getRecurringDepositTemplate(@QueryParam("officeId") final Long officeId, @QueryParam("staffId") final Long staffId, - @QueryParam("dateFormat") final String dateFormat) { - return bulkImportWorkbookPopulatorService.getTemplate(GlobalEntityType.RECURRING_DEPOSIT_ACCOUNTS.toString(), officeId, staffId, - dateFormat); - } - - @POST - @Path("uploadtemplate") - @Consumes(MediaType.MULTIPART_FORM_DATA) - @RequestBody(description = "Upload recurring deposit template", content = { - @Content(mediaType = MediaType.MULTIPART_FORM_DATA, schema = @Schema(implementation = UploadRequest.class)) }) - public String postRecurringDepositTemplate(@FormDataParam("file") InputStream uploadedInputStream, - @FormDataParam("file") FormDataContentDisposition fileDetail, @FormDataParam("locale") final String locale, - @FormDataParam("dateFormat") final String dateFormat) { - final Long importDocumentId = this.bulkImportWorkbookService.importWorkbook(GlobalEntityType.RECURRING_DEPOSIT_ACCOUNTS.toString(), - uploadedInputStream, fileDetail, locale, dateFormat); - return this.toApiJsonSerializer.serialize(importDocumentId); - } - - @GET - @Path("transactions/downloadtemplate") - @Produces("application/vnd.ms-excel") - public Response getRecurringDepositTransactionTemplate(@QueryParam("officeId") final Long officeId, - @QueryParam("dateFormat") final String dateFormat) { - return bulkImportWorkbookPopulatorService.getTemplate(GlobalEntityType.RECURRING_DEPOSIT_ACCOUNTS_TRANSACTIONS.toString(), officeId, - null, dateFormat); - } - - @POST - @Path("transactions/uploadtemplate") - @Consumes(MediaType.MULTIPART_FORM_DATA) - @RequestBody(description = "Upload recurring deposit transactions template", content = { - @Content(mediaType = MediaType.MULTIPART_FORM_DATA, schema = @Schema(implementation = UploadRequest.class)) }) - public String postRecurringDepositTransactionsTemplate(@FormDataParam("file") InputStream uploadedInputStream, - @FormDataParam("file") FormDataContentDisposition fileDetail, @FormDataParam("locale") final String locale, - @FormDataParam("dateFormat") final String dateFormat) { - final Long importDocumentId = this.bulkImportWorkbookService.importWorkbook( - GlobalEntityType.RECURRING_DEPOSIT_ACCOUNTS_TRANSACTIONS.toString(), uploadedInputStream, fileDetail, locale, dateFormat); - return this.toApiJsonSerializer.serialize(importDocumentId); - } -} +/** + * 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.api; + +import com.google.gson.JsonElement; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; +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.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.DefaultValue; +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.Response; +import jakarta.ws.rs.core.UriInfo; +import java.io.InputStream; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +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.bulkimport.data.GlobalEntityType; +import org.apache.fineract.infrastructure.bulkimport.service.BulkImportWorkbookPopulatorService; +import org.apache.fineract.infrastructure.bulkimport.service.BulkImportWorkbookService; +import org.apache.fineract.infrastructure.core.api.ApiParameterHelper; +import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper; +import org.apache.fineract.infrastructure.core.api.JsonQuery; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.data.PaginationParameters; +import org.apache.fineract.infrastructure.core.data.UploadRequest; +import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException; +import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings; +import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.infrastructure.core.service.Page; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.infrastructure.security.service.SqlValidator; +import org.apache.fineract.portfolio.savings.DepositAccountType; +import org.apache.fineract.portfolio.savings.DepositsApiConstants; +import org.apache.fineract.portfolio.savings.SavingsApiConstants; +import org.apache.fineract.portfolio.savings.data.DepositAccountData; +import org.apache.fineract.portfolio.savings.data.RecurringDepositAccountData; +import org.apache.fineract.portfolio.savings.data.SavingsAccountChargeData; +import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData; +import org.apache.fineract.portfolio.savings.service.DepositAccountPreMatureCalculationPlatformService; +import org.apache.fineract.portfolio.savings.service.DepositAccountReadPlatformService; +import org.apache.fineract.portfolio.savings.service.RecurringDepositAccountInterestCalculationService; +import org.apache.fineract.portfolio.savings.service.SavingsAccountChargeReadPlatformService; +import org.glassfish.jersey.media.multipart.FormDataContentDisposition; +import org.glassfish.jersey.media.multipart.FormDataParam; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; + +@Path("/v1/recurringdepositaccounts") +@Component +@Tag(name = "Recurring Deposit Account", description = "Recurring Deposit accounts are instances of a praticular recurring deposit product created. An application process around the creation of accounts is also supported.") +@RequiredArgsConstructor +public class RecurringDepositAccountsApiResource { + + private final DepositAccountReadPlatformService depositAccountReadPlatformService; + private final PlatformSecurityContext context; + private final DefaultToApiJsonSerializer toApiJsonSerializer; + private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; + private final ApiRequestParameterHelper apiRequestParameterHelper; + private final SavingsAccountChargeReadPlatformService savingsAccountChargeReadPlatformService; + private final FromJsonHelper fromJsonHelper; + private final DepositAccountPreMatureCalculationPlatformService accountPreMatureCalculationPlatformService; + private final BulkImportWorkbookService bulkImportWorkbookService; + private final BulkImportWorkbookPopulatorService bulkImportWorkbookPopulatorService; + private final RecurringDepositAccountInterestCalculationService recurringDepositAccountInterestCalculationService; + private final SqlValidator sqlValidator; + + @GET + @Path("template") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Retrieve recurring Deposit Account Template", description = "This is a convenience resource. It can be useful when building maintenance user interface screens for recurring deposit applications. The template data returned consists of any or all of:\n" + + "\n" + "Field Defaults\n" + "Allowed Value Lists\n\n" + "Example Requests:\n" + "\n" + + "recurringdepositaccounts/template?clientId=1\n" + "\n" + "\n" + "recurringdepositaccounts/template?clientId=1&productId=1") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = RecurringDepositAccountsApiResourceSwagger.GetRecurringDepositAccountsTemplateResponse.class))) }) + public String template(@QueryParam("clientId") @Parameter(description = "clientId") final Long clientId, + @QueryParam("groupId") @Parameter(description = "groupId") final Long groupId, + @QueryParam("productId") @Parameter(description = "productId") final Long productId, + @DefaultValue("false") @QueryParam("staffInSelectedOfficeOnly") @Parameter(description = "staffInSelectedOfficeOnly") final boolean staffInSelectedOfficeOnly, + @Context final UriInfo uriInfo) { + + this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME); + + final DepositAccountData account = this.depositAccountReadPlatformService.retrieveTemplate(DepositAccountType.RECURRING_DEPOSIT, + clientId, groupId, productId, staffInSelectedOfficeOnly); + + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + return this.toApiJsonSerializer.serialize(settings, account, + DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS); + } + + @GET + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "List Recurring deposit applications/accounts", description = "Lists Recurring deposit applications/accounts\n\n" + + "Example Requests:\n" + "\n" + "recurringdepositaccounts\n" + "\n" + "\n" + "recurringdepositaccounts?fields=name") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = RecurringDepositAccountsApiResourceSwagger.GetRecurringDepositAccountsResponse.class)))) }) + public String retrieveAll(@Context final UriInfo uriInfo, @QueryParam("paged") @Parameter(description = "paged") final Boolean paged, + @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("accountNo") @Parameter(description = "accountNo") final String accountNo, + @QueryParam("clientName") @Parameter(description = "clientName") final String clientName) { + + this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME); + sqlValidator.validate(orderBy); + sqlValidator.validate(sortOrder); + final PaginationParameters paginationParameters = PaginationParameters.builder().paged(Boolean.TRUE.equals(paged)).limit(limit) + .offset(offset).orderBy(orderBy).sortOrder(sortOrder).build(); + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + + if (paginationParameters.isPaged()) { + final Page account = this.depositAccountReadPlatformService + .retrieveAllPaged(DepositAccountType.RECURRING_DEPOSIT, paginationParameters, accountNo, clientName); + return this.toApiJsonSerializer.serialize(settings, account, + DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS); + } + + final Collection account = this.depositAccountReadPlatformService + .retrieveAll(DepositAccountType.RECURRING_DEPOSIT, paginationParameters); + + return this.toApiJsonSerializer.serialize(settings, account, + DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS); + } + + @POST + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Submit new recurring deposit application", description = "Submits new recurring deposit application\n\n" + + "Mandatory Fields: clientId or groupId, productId, submittedOnDate, depositAmount, depositPeriod, depositPeriodFrequencyId\n\n" + + "Optional Fields: accountNo, externalId, fieldOfficerId,linkAccountId(if provided initial deposit amount will be collected from this account),transferInterestToSavings(By enabling this flag all interest postings will be transferred to linked saving account )\n\n" + + "Inherited from Product (if not provided): interestCompoundingPeriodType, interestCalculationType, interestCalculationDaysInYearType, lockinPeriodFrequency, lockinPeriodFrequencyType, preClosurePenalApplicable, preClosurePenalInterest, preClosurePenalInterestOnTypeId, charts, withHoldTax") + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = RecurringDepositAccountsApiResourceSwagger.PostRecurringDepositAccountsRequest.class))) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = RecurringDepositAccountsApiResourceSwagger.PostRecurringDepositAccountsResponse.class))) }) + public String submitApplication(@Parameter(hidden = true) final String apiRequestBodyAsJson) { + + final CommandWrapper commandRequest = new CommandWrapperBuilder().createRecurringDepositAccount().withJson(apiRequestBodyAsJson) + .build(); + + final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + + return this.toApiJsonSerializer.serialize(result); + } + + @GET + @Path("{accountId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Retrieve a recurring deposit application/account", description = "Retrieves a recurring deposit application/account\n\n" + + "Example Requests :\n" + "\n" + "recurringdepositaccounts/1\n" + "\n" + "\n" + "recurringdepositaccounts/1?associations=all") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = RecurringDepositAccountsApiResourceSwagger.GetRecurringDepositAccountsAccountIdResponse.class))) }) + public String retrieveOne(@PathParam("accountId") @Parameter(description = "accountId") final Long accountId, + @DefaultValue("false") @QueryParam("staffInSelectedOfficeOnly") @Parameter(description = "staffInSelectedOfficeOnly") final boolean staffInSelectedOfficeOnly, + @DefaultValue("all") @QueryParam("chargeStatus") @Parameter(description = "chargeStatus") final String chargeStatus, + @Context final UriInfo uriInfo) { + + this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME); + + if (!(is(chargeStatus, "all") || is(chargeStatus, "active") || is(chargeStatus, "inactive"))) { + throw new UnrecognizedQueryParamException("status", chargeStatus, new Object[] { "all", "active", "inactive" }); + } + + final RecurringDepositAccountData account = (RecurringDepositAccountData) this.depositAccountReadPlatformService + .retrieveOneWithChartSlabs(DepositAccountType.RECURRING_DEPOSIT, accountId); + + final Set mandatoryResponseParameters = new HashSet<>(); + final RecurringDepositAccountData accountTemplate = populateTemplateAndAssociations(accountId, account, staffInSelectedOfficeOnly, + chargeStatus, uriInfo, mandatoryResponseParameters); + + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters(), + mandatoryResponseParameters); + return this.toApiJsonSerializer.serialize(settings, accountTemplate, + DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS); + } + + private RecurringDepositAccountData populateTemplateAndAssociations(final Long accountId, + final RecurringDepositAccountData savingsAccount, final boolean staffInSelectedOfficeOnly, final String chargeStatus, + final UriInfo uriInfo, final Set mandatoryResponseParameters) { + + Collection transactions = null; + Collection charges = null; + + final Set associationParameters = ApiParameterHelper.extractAssociationsForResponseIfProvided(uriInfo.getQueryParameters()); + if (!associationParameters.isEmpty()) { + + if (associationParameters.contains("all")) { + associationParameters.addAll(Arrays.asList(SavingsApiConstants.transactions, SavingsApiConstants.charges)); + } + + if (associationParameters.contains(SavingsApiConstants.transactions)) { + mandatoryResponseParameters.add(SavingsApiConstants.transactions); + final Collection currentTransactions = this.depositAccountReadPlatformService + .retrieveAllTransactions(DepositAccountType.RECURRING_DEPOSIT, accountId); + if (!CollectionUtils.isEmpty(currentTransactions)) { + transactions = currentTransactions; + } + } + + if (associationParameters.contains(SavingsApiConstants.charges)) { + mandatoryResponseParameters.add(SavingsApiConstants.charges); + final Collection currentCharges = this.savingsAccountChargeReadPlatformService + .retrieveSavingsAccountCharges(accountId, chargeStatus); + if (!CollectionUtils.isEmpty(currentCharges)) { + charges = currentCharges; + } + } + } + + RecurringDepositAccountData templateData = null; + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + + if (settings.isTemplate()) { + templateData = (RecurringDepositAccountData) this.depositAccountReadPlatformService.retrieveTemplate( + DepositAccountType.RECURRING_DEPOSIT, savingsAccount.getClientId(), savingsAccount.getGroupId(), + savingsAccount.getDepositProductId(), staffInSelectedOfficeOnly); + } + + return RecurringDepositAccountData.withTemplateOptions(savingsAccount, templateData, transactions, charges); + } + + @PUT + @Path("{accountId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Modify a recurring deposit application", description = "Recurring deposit application can only be modified when in 'Submitted and pending approval' state. Once the application is approved, the details cannot be changed using this method. Specific api endpoints will be created to allow change of interest detail such as rate, compounding period, posting period etc") + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = RecurringDepositAccountsApiResourceSwagger.PutRecurringDepositAccountsAccountIdRequest.class))) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = RecurringDepositAccountsApiResourceSwagger.PutRecurringDepositAccountsAccountIdResponse.class))) }) + public String update(@PathParam("accountId") @Parameter(description = "accountId") final Long accountId, + @Parameter(hidden = true) final String apiRequestBodyAsJson) { + + final CommandWrapper commandRequest = new CommandWrapperBuilder().updateRecurringDepositAccount(accountId) + .withJson(apiRequestBodyAsJson).build(); + + final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + + return this.toApiJsonSerializer.serialize(result); + } + + @POST + @Path("{accountId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Approve recurring deposit application | Undo approval recurring deposit application | Reject recurring deposit application | Withdraw recurring deposit application | Activate a recurring deposit account | Update the recommended deposit amount for a recurring deposit account | Close a recurring deposit account | Premature Close a recurring deposit account | Calculate Premature amount on Recurring deposit account | Calculate Interest on recurring Deposit Account | Post Interest on recurring Deposit Account", description = "Approve recurring deposit application:\n\n" + + "Approves recurring deposit application so long as its in 'Submitted and pending approval' state.\n\n" + + "Undo approval recurring deposit application:\n\n" + + "Will move 'approved' recurring deposit application back to 'Submitted and pending approval' state.\n\n" + + "Reject recurring deposit application\n\n" + + "Rejects recurring deposit application so long as its in 'Submitted and pending approval' state.\n\n" + + "Withdraw recurring deposit application:\n\n" + + "Used when an applicant withdraws from the recurring deposit application. It must be in 'Submitted and pending approval' state.\n\n" + + "Activate a recurring deposit account:\n\n" + + "Results in an approved recurring deposit application being converted into an 'active' recurring deposit account.\n\n" + + "Update the recommended deposit amount for a recurring deposit account:\n\n" + + "Updates the recommended deposit amount for a RD account as on the effective date.\n\n" + + "Close a recurring deposit account\n\n" + + "Results in a Matured recurring deposit account being converted into a 'closed' recurring deposit account.\n" + "\n" + + "On account close allowed actions are." + "Premature Close a recurring deposit account:\n\n" + + "Results in an Active recurring deposit account being converted into a 'Premature Closed' recurring deposit account with options to withdraw prematured amount. (premature amount is calculated using interest rate chart applicable along with penal interest if any.)\n" + + "\n" + "On account premature closure allowed actions are.\n\n" + + "Calculate Premature amount on Recurring deposit account:\n\n" + + "Calculate premature amount on recurring deposit till premature close date. Premature amount is calculated based on interest chart and penal interest applicable if any.\n\n" + + "Calculate Interest on recurring Deposit Account:\n\n" + + "Calculates interest earned on a recurring deposit account based on todays date. It does not attempt to post or credit the interest on the account. That is responsibility of the Post Interest API that will likely be called by overnight process.\n\n" + + "Post Interest on recurring Deposit Account:\n\n" + + "Calculates and Posts interest earned on a recurring deposit account based on todays date and whether an interest posting or crediting event is due.\n\n" + + "Showing request/response for 'Post Interest on recurring Deposit Account'") + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = RecurringDepositAccountsApiResourceSwagger.PostRecurringDepositAccountsAccountIdRequest.class))) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = RecurringDepositAccountsApiResourceSwagger.PostRecurringDepositAccountsAccountIdResponse.class))) }) + public String handleCommands(@PathParam("accountId") @Parameter(description = "accountId") final Long accountId, + @QueryParam("command") @Parameter(description = "command") final String commandParam, @Context final UriInfo uriInfo, + @Parameter(hidden = true) final String apiRequestBodyAsJson) { + + String jsonApiRequest = apiRequestBodyAsJson; + if (StringUtils.isBlank(jsonApiRequest)) { + jsonApiRequest = "{}"; + } + + final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(jsonApiRequest); + + CommandProcessingResult result = null; + if (is(commandParam, "reject")) { + final CommandWrapper commandRequest = builder.rejectRecurringDepositAccountApplication(accountId).build(); + result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (is(commandParam, "withdrawnByApplicant")) { + final CommandWrapper commandRequest = builder.withdrawRecurringDepositAccountApplication(accountId).build(); + result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (is(commandParam, "approve")) { + final CommandWrapper commandRequest = builder.approveRecurringDepositAccountApplication(accountId).build(); + result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (is(commandParam, "undoapproval")) { + final CommandWrapper commandRequest = builder.undoRecurringDepositAccountApplication(accountId).build(); + result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (is(commandParam, "activate")) { + final CommandWrapper commandRequest = builder.recurringDepositAccountActivation(accountId).build(); + result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (is(commandParam, "calculateInterest")) { + final CommandWrapper commandRequest = builder.withNoJsonBody().recurringDepositAccountInterestCalculation(accountId).build(); + result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (is(commandParam, DepositsApiConstants.UPDATE_DEPOSIT_AMOUNT)) { + final CommandWrapper commandRequest = builder.updateDepositAmountForRecurringDepositAccount(accountId) + .withJson(apiRequestBodyAsJson).build(); + result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (is(commandParam, "postInterest")) { + final CommandWrapper commandRequest = builder.recurringDepositAccountInterestPosting(accountId).build(); + result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (is(commandParam, "close")) { + final CommandWrapper commandRequest = builder.closeRecurringDepositAccount(accountId).build(); + result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (is(commandParam, "prematureClose")) { + final CommandWrapper commandRequest = builder.prematureCloseRecurringDepositAccount(accountId).build(); + result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (is(commandParam, "calculatePrematureAmount")) { + final JsonElement parsedQuery = this.fromJsonHelper.parse(apiRequestBodyAsJson); + final JsonQuery query = JsonQuery.from(apiRequestBodyAsJson, parsedQuery, this.fromJsonHelper); + final DepositAccountData account = this.accountPreMatureCalculationPlatformService.calculatePreMatureAmount(accountId, query, + DepositAccountType.RECURRING_DEPOSIT); + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + return this.toApiJsonSerializer.serialize(settings, account, + DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS); + } + + if (result == null) { + throw new UnrecognizedQueryParamException("command", commandParam, + new Object[] { "reject", "withdrawnByApplicant", "approve", "undoapproval", "activate", "calculateInterest", + "postInterest", "close", "prematureClose", "calculatePrematureAmount" }); + } + + return this.toApiJsonSerializer.serialize(result); + } + + @POST + @Path("calculate-rd-interest") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String calculateRecurringDepositInterest(@Context final UriInfo uriInfo, final String apiRequestBodyAsJson) { + JsonElement jsonElement = fromJsonHelper.parse(apiRequestBodyAsJson); + HashMap result = recurringDepositAccountInterestCalculationService + .calculateInterest(new JsonQuery(apiRequestBodyAsJson, jsonElement, fromJsonHelper)); + + return toApiJsonSerializer.serializeResult(result); + } + + private boolean is(final String commandParam, final String commandValue) { + return StringUtils.isNotBlank(commandParam) && commandParam.trim().equalsIgnoreCase(commandValue); + } + + @DELETE + @Path("{accountId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Delete a recurring deposit application", description = "At present we support hard delete of recurring deposit application so long as its in 'Submitted and pending approval' state. One the application is moves past this state, it is not possible to do a 'hard' delete of the application or the account. An API endpoint will be added to close/de-activate the recurring deposit account.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = RecurringDepositAccountsApiResourceSwagger.DeleteRecurringDepositAccountsResponse.class))) }) + public String delete(@PathParam("accountId") @Parameter(description = "accountId") final Long accountId) { + + final CommandWrapper commandRequest = new CommandWrapperBuilder().deleteRecurringDepositAccount(accountId).build(); + + final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + + return this.toApiJsonSerializer.serialize(result); + } + + @GET + @Path("{accountId}/template") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String accountClosureTemplate(@PathParam("accountId") @Parameter(description = "accountId") final Long accountId, + @QueryParam("command") @Parameter(description = "command") final String commandParam, @Context final UriInfo uriInfo) { + + this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME); + DepositAccountData account = null; + if (is(commandParam, "close")) { + account = this.depositAccountReadPlatformService.retrieveOneWithClosureTemplate(DepositAccountType.RECURRING_DEPOSIT, + accountId); + + } + + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + return this.toApiJsonSerializer.serialize(settings, account, + DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS); + } + + @GET + @Path("downloadtemplate") + @Produces("application/vnd.ms-excel") + public Response getRecurringDepositTemplate(@QueryParam("officeId") final Long officeId, @QueryParam("staffId") final Long staffId, + @QueryParam("dateFormat") final String dateFormat) { + return bulkImportWorkbookPopulatorService.getTemplate(GlobalEntityType.RECURRING_DEPOSIT_ACCOUNTS.toString(), officeId, staffId, + dateFormat); + } + + @POST + @Path("uploadtemplate") + @Consumes(MediaType.MULTIPART_FORM_DATA) + @RequestBody(description = "Upload recurring deposit template", content = { + @Content(mediaType = MediaType.MULTIPART_FORM_DATA, schema = @Schema(implementation = UploadRequest.class)) }) + public String postRecurringDepositTemplate(@FormDataParam("file") InputStream uploadedInputStream, + @FormDataParam("file") FormDataContentDisposition fileDetail, @FormDataParam("locale") final String locale, + @FormDataParam("dateFormat") final String dateFormat) { + final Long importDocumentId = this.bulkImportWorkbookService.importWorkbook(GlobalEntityType.RECURRING_DEPOSIT_ACCOUNTS.toString(), + uploadedInputStream, fileDetail, locale, dateFormat); + return this.toApiJsonSerializer.serialize(importDocumentId); + } + + @GET + @Path("transactions/downloadtemplate") + @Produces("application/vnd.ms-excel") + public Response getRecurringDepositTransactionTemplate(@QueryParam("officeId") final Long officeId, + @QueryParam("dateFormat") final String dateFormat) { + return bulkImportWorkbookPopulatorService.getTemplate(GlobalEntityType.RECURRING_DEPOSIT_ACCOUNTS_TRANSACTIONS.toString(), officeId, + null, dateFormat); + } + + @POST + @Path("transactions/uploadtemplate") + @Consumes(MediaType.MULTIPART_FORM_DATA) + @RequestBody(description = "Upload recurring deposit transactions template", content = { + @Content(mediaType = MediaType.MULTIPART_FORM_DATA, schema = @Schema(implementation = UploadRequest.class)) }) + public String postRecurringDepositTransactionsTemplate(@FormDataParam("file") InputStream uploadedInputStream, + @FormDataParam("file") FormDataContentDisposition fileDetail, @FormDataParam("locale") final String locale, + @FormDataParam("dateFormat") final String dateFormat) { + final Long importDocumentId = this.bulkImportWorkbookService.importWorkbook( + GlobalEntityType.RECURRING_DEPOSIT_ACCOUNTS_TRANSACTIONS.toString(), uploadedInputStream, fileDetail, locale, dateFormat); + return this.toApiJsonSerializer.serialize(importDocumentId); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountsApiResource.java index 2630c5351..2107cba03 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountsApiResource.java @@ -1,643 +1,646 @@ -/** - * 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.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.Consumes; -import jakarta.ws.rs.DELETE; -import jakarta.ws.rs.DefaultValue; -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.Response; -import jakarta.ws.rs.core.UriInfo; -import java.io.InputStream; -import java.util.Arrays; -import java.util.Collection; -import java.util.Set; -import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.StringUtils; -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.bulkimport.data.GlobalEntityType; -import org.apache.fineract.infrastructure.bulkimport.service.BulkImportWorkbookPopulatorService; -import org.apache.fineract.infrastructure.bulkimport.service.BulkImportWorkbookService; -import org.apache.fineract.infrastructure.core.api.ApiParameterHelper; -import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper; -import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; -import org.apache.fineract.infrastructure.core.data.UploadRequest; -import org.apache.fineract.infrastructure.core.domain.ExternalId; -import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException; -import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings; -import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; -import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; -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.savings.DepositAccountType; -import org.apache.fineract.portfolio.savings.SavingsApiConstants; -import org.apache.fineract.portfolio.savings.data.SavingsAccountChargeData; -import org.apache.fineract.portfolio.savings.data.SavingsAccountData; -import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData; -import org.apache.fineract.portfolio.savings.exception.SavingsAccountNotFoundException; -import org.apache.fineract.portfolio.savings.service.SavingsAccountChargeReadPlatformService; -import org.apache.fineract.portfolio.savings.service.SavingsAccountReadPlatformService; -import org.apache.fineract.portfolio.savings.service.SavingsAccountTemplateReadPlatformService; -import org.glassfish.jersey.media.multipart.FormDataContentDisposition; -import org.glassfish.jersey.media.multipart.FormDataParam; -import org.springframework.stereotype.Component; -import org.springframework.util.CollectionUtils; - -@Path("/v1/savingsaccounts") -@Component -@Tag(name = "Savings Account", description = "Savings accounts are instances of a particular savings product created for an individual or group. An application process around the creation of accounts is also supported.") -@RequiredArgsConstructor -public class SavingsAccountsApiResource { - - private final SavingsAccountReadPlatformService savingsAccountReadPlatformService; - private final SavingsAccountTemplateReadPlatformService savingsAccountTemplateReadPlatformService; - private final PlatformSecurityContext context; - private final DefaultToApiJsonSerializer toApiJsonSerializer; - private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; - private final ApiRequestParameterHelper apiRequestParameterHelper; - private final SavingsAccountChargeReadPlatformService savingsAccountChargeReadPlatformService; - private final BulkImportWorkbookService bulkImportWorkbookService; - private final BulkImportWorkbookPopulatorService bulkImportWorkbookPopulatorService; - private final SqlValidator sqlValidator; - - @GET - @Path("template") - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - @Operation(summary = "Retrieve Savings Account 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\n" + "Example Requests:\n" + "\n" + "savingsaccounts/template?clientId=1\n" - + "\n" + "\n" + "savingsaccounts/template?clientId=1&productId=1") - @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = SavingsAccountsApiResourceSwagger.GetSavingsAccountsTemplateResponse.class))) - public String template(@QueryParam("clientId") @Parameter(description = "clientId") final Long clientId, - @QueryParam("groupId") @Parameter(description = "groupId") final Long groupId, - @QueryParam("productId") @Parameter(description = "productId") final Long productId, - @DefaultValue("false") @QueryParam("staffInSelectedOfficeOnly") @Parameter(description = "staffInSelectedOfficeOnly") final boolean staffInSelectedOfficeOnly, - @Context final UriInfo uriInfo) { - - context.authenticatedUser().validateHasReadPermission(SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME); - - final SavingsAccountData savingsAccount = savingsAccountTemplateReadPlatformService.retrieveTemplate(clientId, groupId, productId, - staffInSelectedOfficeOnly); - - final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters()); - return toApiJsonSerializer.serialize(settings, savingsAccount, SavingsApiSetConstants.SAVINGS_ACCOUNT_RESPONSE_DATA_PARAMETERS); - } - - @GET - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - @Operation(summary = "List savings applications/accounts", description = "Lists savings applications/accounts\n\n" - + "Example Requests:\n" + "\n" + "savingsaccounts\n" + "\n" + "\n" + "savingsaccounts?fields=name") - @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = SavingsAccountsApiResourceSwagger.GetSavingsAccountsResponse.class))) - public String retrieveAll(@Context final UriInfo uriInfo, - @QueryParam("externalId") @Parameter(description = "externalId") final String externalId, - // @QueryParam("underHierarchy") final String hierarchy, - @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(SavingsApiConstants.SAVINGS_ACCOUNT_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 Page products = savingsAccountReadPlatformService.retrieveAll(searchParameters); - - final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters()); - return toApiJsonSerializer.serialize(settings, products, SavingsApiSetConstants.SAVINGS_ACCOUNT_RESPONSE_DATA_PARAMETERS); - } - - @POST - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - @Operation(summary = "Submit new savings application", description = "Submits new savings application\n\n" - + "Mandatory Fields: clientId or groupId, productId, submittedOnDate\n\n" - + "Optional Fields: accountNo, externalId, fieldOfficerId\n\n" - + "Inherited from Product (if not provided): nominalAnnualInterestRate, interestCompoundingPeriodType, interestCalculationType, interestCalculationDaysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType, withdrawalFeeForTransfers, allowOverdraft, overdraftLimit, withHoldTax\n\n" - + "Additional Mandatory Field if Entity-Datatable Check is enabled for the entity of type Savings: datatables") - @RequestBody(required = true, content = @Content(schema = @Schema(implementation = SavingsAccountsApiResourceSwagger.PostSavingsAccountsRequest.class))) - @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = SavingsAccountsApiResourceSwagger.PostSavingsAccountsResponse.class))) - public String submitApplication(@Parameter(hidden = true) final String apiRequestBodyAsJson) { - - final CommandWrapper commandRequest = new CommandWrapperBuilder().createSavingsAccount().withJson(apiRequestBodyAsJson).build(); - - final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest); - - return toApiJsonSerializer.serialize(result); - } - - @POST - @Path("/gsim") - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - public String submitGSIMApplication(final String apiRequestBodyAsJson) { - - final CommandWrapper commandRequest = new CommandWrapperBuilder().createGSIMAccount().withJson(apiRequestBodyAsJson).build(); - - final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest); - - return toApiJsonSerializer.serialize(result); - } - - @GET - @Path("{accountId}") - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - public SavingsAccountData retrieveOne(@PathParam("accountId") final Long accountId, - @DefaultValue("false") @QueryParam("staffInSelectedOfficeOnly") final boolean staffInSelectedOfficeOnly, - @DefaultValue("all") @QueryParam("chargeStatus") final String chargeStatus, - @QueryParam("associations") final String associations, @Context final UriInfo uriInfo) { - - return retrieveSavingAccount(accountId, null, staffInSelectedOfficeOnly, chargeStatus, uriInfo); - } - - @GET - @Path("/external-id/{externalId}") - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - public SavingsAccountData retrieveOne(@PathParam("externalId") final String externalId, - @DefaultValue("false") @QueryParam("staffInSelectedOfficeOnly") final boolean staffInSelectedOfficeOnly, - @DefaultValue("all") @QueryParam("chargeStatus") final String chargeStatus, - @QueryParam("associations") final String associations, @Context final UriInfo uriInfo) { - - return retrieveSavingAccount(null, externalId, staffInSelectedOfficeOnly, chargeStatus, uriInfo); - } - - @PUT - @Path("{accountId}") - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - @Operation(summary = "Modify a savings application | Modify savings account withhold tax applicability", description = "Modify a savings application:\n\n" - + "Savings application can only be modified when in 'Submitted and pending approval' state. Once the application is approved, the details cannot be changed using this method. Specific api endpoints will be created to allow change of interest detail such as rate, compounding period, posting period etc\n\n" - + "Modify savings account withhold tax applicability:\n\n" - + "Savings application's withhold tax can be modified when in 'Active' state. Once the application is activated, can modify the account withhold tax to post tax or vice-versa" - + "Showing request/response for 'Modify a savings application'") - @RequestBody(required = true, content = @Content(schema = @Schema(implementation = SavingsAccountsApiResourceSwagger.PutSavingsAccountsAccountIdRequest.class))) - @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = SavingsAccountsApiResourceSwagger.PutSavingsAccountsAccountIdResponse.class))) - public String update(@PathParam("accountId") @Parameter(description = "accountId") final Long accountId, - @Parameter(hidden = true) final String apiRequestBodyAsJson, - @QueryParam("command") @Parameter(description = "command") final String commandParam) { - - return updateSavingAccount(accountId, null, apiRequestBodyAsJson, commandParam); - } - - @PUT - @Path("/external-id/{externalId}") - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - @Operation(summary = "Modify a savings application | Modify savings account withhold tax applicability", description = "Modify a savings application:\n\n" - + "Savings application can only be modified when in 'Submitted and pending approval' state. Once the application is approved, the details cannot be changed using this method. Specific api endpoints will be created to allow change of interest detail such as rate, compounding period, posting period etc\n\n" - + "Modify savings account withhold tax applicability:\n\n" - + "Savings application's withhold tax can be modified when in 'Active' state. Once the application is activated, can modify the account withhold tax to post tax or vice-versa" - + "Showing request/response for 'Modify a savings application'") - @RequestBody(required = true, content = @Content(schema = @Schema(implementation = SavingsAccountsApiResourceSwagger.PutSavingsAccountsAccountIdRequest.class))) - @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = SavingsAccountsApiResourceSwagger.PutSavingsAccountsAccountIdResponse.class))) - public String update(@PathParam("externalId") @Parameter(description = "externalId") final String externalId, - @Parameter(hidden = true) final String apiRequestBodyAsJson, - @QueryParam("command") @Parameter(description = "command") final String commandParam) { - - return updateSavingAccount(null, externalId, apiRequestBodyAsJson, commandParam); - } - - @PUT - @Path("/gsim/{parentAccountId}") - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - public String updateGsim(@PathParam("parentAccountId") final Long parentAccountId, final String apiRequestBodyAsJson) { - final CommandWrapper commandRequest = new CommandWrapperBuilder().updateGSIMAccount(parentAccountId).withJson(apiRequestBodyAsJson) - .build(); - - final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest); - - return toApiJsonSerializer.serialize(result); - } - - @POST - @Path("/gsimcommands/{parentAccountId}") - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - public String handleGSIMCommands(@PathParam("parentAccountId") final Long parentAccountId, - @QueryParam("command") final String commandParam, final String apiRequestBodyAsJson) { - - String jsonApiRequest = apiRequestBodyAsJson; - if (StringUtils.isBlank(jsonApiRequest)) { - jsonApiRequest = "{}"; - } - - final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(jsonApiRequest); - - CommandProcessingResult result = null; - if (is(commandParam, "reject")) { - final CommandWrapper commandRequest = builder.rejectGSIMAccountApplication(parentAccountId).build(); - result = commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, "withdrawnByApplicant")) { - final CommandWrapper commandRequest = builder.withdrawSavingsAccountApplication(parentAccountId).build(); - result = commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, "approve")) { - final CommandWrapper commandRequest = builder.approveGSIMAccountApplication(parentAccountId).build(); - result = commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, "undoapproval")) { - final CommandWrapper commandRequest = builder.undoGSIMApplicationApproval(parentAccountId).build(); - result = commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, "activate")) { - final CommandWrapper commandRequest = builder.gsimAccountActivation(parentAccountId).build(); - result = commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, "calculateInterest")) { - final CommandWrapper commandRequest = builder.withNoJsonBody().savingsAccountInterestCalculation(parentAccountId).build(); - result = commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, "postInterest")) { - final CommandWrapper commandRequest = builder.savingsAccountInterestPosting(parentAccountId).build(); - result = commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, "applyAnnualFees")) { - final CommandWrapper commandRequest = builder.savingsAccountApplyAnnualFees(parentAccountId).build(); - result = commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, "close")) { - final CommandWrapper commandRequest = builder.closeGSIMApplication(parentAccountId).build(); - result = commandsSourceWritePlatformService.logCommandSource(commandRequest); - } - if (result == null) { - throw new UnrecognizedQueryParamException("command", commandParam, - new Object[] { "reject", "withdrawnByApplicant", "approve", "undoapproval", "activate", "calculateInterest", - "postInterest", "close", "assignSavingsOfficer", "unassignSavingsOfficer", - SavingsApiConstants.COMMAND_BLOCK_DEBIT, SavingsApiConstants.COMMAND_UNBLOCK_DEBIT, - SavingsApiConstants.COMMAND_BLOCK_CREDIT, SavingsApiConstants.COMMAND_UNBLOCK_CREDIT, - SavingsApiConstants.COMMAND_BLOCK_ACCOUNT, SavingsApiConstants.COMMAND_UNBLOCK_ACCOUNT }); - } - - return toApiJsonSerializer.serialize(result); - } - - @POST - @Path("{accountId}") - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - @Operation(summary = "Approve savings application | Undo approval savings application | Assign Savings Officer | Unassign Savings Officer | Reject savings application | Withdraw savings application | Activate a savings account | Close a savings account | Calculate Interest on Savings Account | Post Interest on Savings Account | Block Savings Account | Unblock Savings Account | Block Savings Account Credit transactions | Unblock Savings Account Credit transactions | Block Savings Account Debit transactions | Unblock Savings Account debit transactions", description = "Approve savings application:\n\n" - + "Approves savings application so long as its in 'Submitted and pending approval' state.\n\n" - + "Undo approval savings application:\n\n" - + "Will move 'approved' savings application back to 'Submitted and pending approval' state.\n\n" + "Assign Savings Officer:\n\n" - + "Allows you to assign Savings Officer for existing Savings Account.\n\n" + "Unassign Savings Officer:\n\n" - + "Allows you to unassign the Savings Officer.\n\n" + "Reject savings application:\n\n" - + "Rejects savings application so long as its in 'Submitted and pending approval' state.\n\n" - + "Withdraw savings application:\n\n" - + "Used when an applicant withdraws from the savings application. It must be in 'Submitted and pending approval' state.\n\n" - + "Activate a savings account:\n\n" - + "Results in an approved savings application being converted into an 'active' savings account.\n\n" - + "Close a savings account:\n\n" - + "Results in an Activated savings application being converted into an 'closed' savings account.\n" + "\n" - + "closedOnDate is closure date of savings account\n" + "\n" - + "withdrawBalance is a boolean description, true value of this field performs a withdrawal transaction with account's running balance.\n\n" - + "Mandatory Fields: dateFormat,locale,closedOnDate\n\n" - + "Optional Fields: note, withdrawBalance, paymentTypeId, accountNumber, checkNumber, routingCode, receiptNumber, bankNumber\n\n" - + "Calculate Interest on Savings Account:\n\n" - + "Calculates interest earned on a savings account based on todays date. It does not attempt to post or credit the interest on the account. That is responsibility of the Post Interest API that will likely be called by overnight process.\n\n" - + "Post Interest on Savings Account:\n\n" - + "Calculates and Posts interest earned on a savings account based on today's date and whether an interest posting or crediting event is due.\n\n" - + "Block Savings Account:\n\n" + "Blocks Savings account from all types of credit and debit transactions\n\n" - + "Unblock Savings Account:\n\n" - + "Unblock a blocked account. On unblocking account, user can perform debit and credit transactions\n\n" - + "Block Savings Account Credit transactions:\n\n" - + "Savings account will be blocked from all types of credit transactions.\n\n" - + "Unblock Savings Account Credit transactions:\n\n" - + "It unblocks the Saving account's credit operations. Now all types of credits can be transacted to Savings account\n\n" - + "Block Savings Account Debit transactions:\n\n" + "All types of debit operations from Savings account wil be blocked\n\n" - + "Unblock Savings Account debit transactions:\n\n" - + "It unblocks the Saving account's debit operations. Now all types of debits can be transacted from Savings account\n\n" - + "Showing request/response for 'Unassign Savings Officer'") - @RequestBody(required = true, content = @Content(schema = @Schema(implementation = SavingsAccountsApiResourceSwagger.PostSavingsAccountsAccountIdRequest.class))) - @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = SavingsAccountsApiResourceSwagger.PostSavingsAccountsAccountIdResponse.class))) - public String handleCommands(@PathParam("accountId") @Parameter(description = "accountId") final Long accountId, - @QueryParam("command") @Parameter(description = "command") final String commandParam, - @Parameter(hidden = true) final String apiRequestBodyAsJson) { - - return handleCommands(accountId, null, commandParam, apiRequestBodyAsJson); - } - - @POST - @Path("/external-id/{externalId}") - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - @Operation(summary = "Approve savings application | Undo approval savings application | Assign Savings Officer | Unassign Savings Officer | Reject savings application | Withdraw savings application | Activate a savings account | Close a savings account | Calculate Interest on Savings Account | Post Interest on Savings Account | Block Savings Account | Unblock Savings Account | Block Savings Account Credit transactions | Unblock Savings Account Credit transactions | Block Savings Account Debit transactions | Unblock Savings Account debit transactions", description = "Approve savings application:\n\n" - + "Approves savings application so long as its in 'Submitted and pending approval' state.\n\n" - + "Undo approval savings application:\n\n" - + "Will move 'approved' savings application back to 'Submitted and pending approval' state.\n\n" + "Assign Savings Officer:\n\n" - + "Allows you to assign Savings Officer for existing Savings Account.\n\n" + "Unassign Savings Officer:\n\n" - + "Allows you to unassign the Savings Officer.\n\n" + "Reject savings application:\n\n" - + "Rejects savings application so long as its in 'Submitted and pending approval' state.\n\n" - + "Withdraw savings application:\n\n" - + "Used when an applicant withdraws from the savings application. It must be in 'Submitted and pending approval' state.\n\n" - + "Activate a savings account:\n\n" - + "Results in an approved savings application being converted into an 'active' savings account.\n\n" - + "Close a savings account:\n\n" - + "Results in an Activated savings application being converted into an 'closed' savings account.\n" + "\n" - + "closedOnDate is closure date of savings account\n" + "\n" - + "withdrawBalance is a boolean description, true value of this field performs a withdrawal transaction with account's running balance.\n\n" - + "Mandatory Fields: dateFormat,locale,closedOnDate\n\n" - + "Optional Fields: note, withdrawBalance, paymentTypeId, accountNumber, checkNumber, routingCode, receiptNumber, bankNumber\n\n" - + "Calculate Interest on Savings Account:\n\n" - + "Calculates interest earned on a savings account based on todays date. It does not attempt to post or credit the interest on the account. That is responsibility of the Post Interest API that will likely be called by overnight process.\n\n" - + "Post Interest on Savings Account:\n\n" - + "Calculates and Posts interest earned on a savings account based on today's date and whether an interest posting or crediting event is due.\n\n" - + "Block Savings Account:\n\n" + "Blocks Savings account from all types of credit and debit transactions\n\n" - + "Unblock Savings Account:\n\n" - + "Unblock a blocked account. On unblocking account, user can perform debit and credit transactions\n\n" - + "Block Savings Account Credit transactions:\n\n" - + "Savings account will be blocked from all types of credit transactions.\n\n" - + "Unblock Savings Account Credit transactions:\n\n" - + "It unblocks the Saving account's credit operations. Now all types of credits can be transacted to Savings account\n\n" - + "Block Savings Account Debit transactions:\n\n" + "All types of debit operations from Savings account wil be blocked\n\n" - + "Unblock Savings Account debit transactions:\n\n" - + "It unblocks the Saving account's debit operations. Now all types of debits can be transacted from Savings account\n\n" - + "Showing request/response for 'Unassign Savings Officer'") - @RequestBody(required = true, content = @Content(schema = @Schema(implementation = SavingsAccountsApiResourceSwagger.PostSavingsAccountsAccountIdRequest.class))) - @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = SavingsAccountsApiResourceSwagger.PostSavingsAccountsAccountIdResponse.class))) - public String handleCommands(@PathParam("externalId") @Parameter(description = "externalId") final String externalId, - @QueryParam("command") @Parameter(description = "command") final String commandParam, - @Parameter(hidden = true) final String apiRequestBodyAsJson) { - - return handleCommands(null, externalId, commandParam, apiRequestBodyAsJson); - } - - private boolean is(final String commandParam, final String commandValue) { - return StringUtils.isNotBlank(commandParam) && commandParam.trim().equalsIgnoreCase(commandValue); - } - - @DELETE - @Path("{accountId}") - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - @Operation(summary = "Delete a savings application", description = "At present we support hard delete of savings application so long as its in 'Submitted and pending approval' state. One the application is moves past this state, it is not possible to do a 'hard' delete of the application or the account. An API endpoint will be added to close/de-activate the savings account.") - @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = SavingsAccountsApiResourceSwagger.DeleteSavingsAccountsAccountIdResponse.class))) - public String delete(@PathParam("accountId") @Parameter(description = "accountId") final Long accountId) { - - return deleteSavingAccount(accountId, null); - } - - @DELETE - @Path("/external-id/{externalId}") - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - @Operation(summary = "Delete a savings application", description = "At present we support hard delete of savings application so long as its in 'Submitted and pending approval' state. One the application is moves past this state, it is not possible to do a 'hard' delete of the application or the account. An API endpoint will be added to close/de-activate the savings account.") - @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = SavingsAccountsApiResourceSwagger.DeleteSavingsAccountsAccountIdResponse.class))) - public String delete(@PathParam("externalId") @Parameter(description = "externalId") final String externalId) { - - return deleteSavingAccount(null, externalId); - } - - @GET - @Path("downloadtemplate") - @Produces("application/vnd.ms-excel") - public Response getSavingsTemplate(@QueryParam("officeId") final Long officeId, @QueryParam("staffId") final Long staffId, - @QueryParam("dateFormat") final String dateFormat) { - return bulkImportWorkbookPopulatorService.getTemplate(GlobalEntityType.SAVINGS_ACCOUNT.toString(), officeId, staffId, dateFormat); - } - - @POST - @Path("uploadtemplate") - @Consumes(MediaType.MULTIPART_FORM_DATA) - @RequestBody(description = "Upload savings template", content = { - @Content(mediaType = MediaType.MULTIPART_FORM_DATA, schema = @Schema(implementation = UploadRequest.class)) }) - public String postSavingsTemplate(@FormDataParam("file") InputStream uploadedInputStream, - @FormDataParam("file") FormDataContentDisposition fileDetail, @FormDataParam("locale") final String locale, - @FormDataParam("dateFormat") final String dateFormat) { - final Long importDocumentId = bulkImportWorkbookService.importWorkbook(GlobalEntityType.SAVINGS_ACCOUNT.toString(), - uploadedInputStream, fileDetail, locale, dateFormat); - return toApiJsonSerializer.serialize(importDocumentId); - } - - @GET - @Path("transactions/downloadtemplate") - @Produces("application/vnd.ms-excel") - public Response getSavingsTransactionTemplate(@QueryParam("officeId") final Long officeId, - @QueryParam("dateFormat") final String dateFormat) { - return bulkImportWorkbookPopulatorService.getTemplate(GlobalEntityType.SAVINGS_TRANSACTIONS.toString(), officeId, null, dateFormat); - } - - @POST - @Path("transactions/uploadtemplate") - @Consumes(MediaType.MULTIPART_FORM_DATA) - @RequestBody(description = "Upload savings transaction template", content = { - @Content(mediaType = MediaType.MULTIPART_FORM_DATA, schema = @Schema(implementation = UploadRequest.class)) }) - public String postSavingsTransactionTemplate(@FormDataParam("file") InputStream uploadedInputStream, - @FormDataParam("file") FormDataContentDisposition fileDetail, @FormDataParam("locale") final String locale, - @FormDataParam("dateFormat") final String dateFormat) { - final Long importDocumentId = bulkImportWorkbookService.importWorkbook(GlobalEntityType.SAVINGS_TRANSACTIONS.toString(), - uploadedInputStream, fileDetail, locale, dateFormat); - return toApiJsonSerializer.serialize(importDocumentId); - } - - private SavingsAccountData retrieveSavingAccount(Long accountId, String externalId, boolean staffInSelectedOfficeOnly, - String chargeStatus, UriInfo uriInfo) { - context.authenticatedUser().validateHasReadPermission(SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME); - - if (!(is(chargeStatus, "all") || is(chargeStatus, "active") || is(chargeStatus, "inactive"))) { - throw new UnrecognizedQueryParamException("status", chargeStatus, new Object[] { "all", "active", "inactive" }); - } - - ExternalId accountExternalId = ExternalIdFactory.produce(externalId); - accountId = getResolvedAccountId(accountId, accountExternalId); - final SavingsAccountData savingsAccount = savingsAccountReadPlatformService.retrieveOne(accountId); - - return populateTemplateAndAssociations(accountId, savingsAccount, staffInSelectedOfficeOnly, chargeStatus, uriInfo); - } - - private String updateSavingAccount(Long accountId, String externalId, String apiRequestBodyAsJson, String commandParam) { - ExternalId accountExternalId = ExternalIdFactory.produce(externalId); - accountId = getResolvedAccountId(accountId, accountExternalId); - - if (is(commandParam, "updateWithHoldTax")) { - final CommandWrapper commandRequest = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson).updateWithHoldTax(accountId) - .build(); - final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest); - return toApiJsonSerializer.serialize(result); - } - - final CommandWrapper commandRequest = new CommandWrapperBuilder().updateSavingsAccount(accountId).withJson(apiRequestBodyAsJson) - .build(); - - final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest); - - return toApiJsonSerializer.serialize(result); - } - - private String handleCommands(Long accountId, String externalId, String commandParam, String apiRequestBodyAsJson) { - ExternalId accountExternalId = ExternalIdFactory.produce(externalId); - accountId = getResolvedAccountId(accountId, accountExternalId); - - String jsonApiRequest = apiRequestBodyAsJson; - if (StringUtils.isBlank(jsonApiRequest)) { - jsonApiRequest = "{}"; - } - - final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(jsonApiRequest); - - CommandProcessingResult result = null; - if (is(commandParam, "reject")) { - final CommandWrapper commandRequest = builder.rejectSavingsAccountApplication(accountId).build(); - result = commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, "withdrawnByApplicant")) { - final CommandWrapper commandRequest = builder.withdrawSavingsAccountApplication(accountId).build(); - result = commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, "approve")) { - final CommandWrapper commandRequest = builder.approveSavingsAccountApplication(accountId).build(); - result = commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, "undoapproval")) { - final CommandWrapper commandRequest = builder.undoSavingsAccountApplication(accountId).build(); - result = commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, "activate")) { - final CommandWrapper commandRequest = builder.savingsAccountActivation(accountId).build(); - result = commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, "calculateInterest")) { - final CommandWrapper commandRequest = builder.withNoJsonBody().savingsAccountInterestCalculation(accountId).build(); - result = commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, "postInterest")) { - final CommandWrapper commandRequest = builder.savingsAccountInterestPosting(accountId).build(); - result = commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, "applyAnnualFees")) { - final CommandWrapper commandRequest = builder.savingsAccountApplyAnnualFees(accountId).build(); - result = commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, "close")) { - final CommandWrapper commandRequest = builder.closeSavingsAccountApplication(accountId).build(); - result = commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, "assignSavingsOfficer")) { - final CommandWrapper commandRequest = builder.assignSavingsOfficer(accountId).build(); - result = commandsSourceWritePlatformService.logCommandSource(commandRequest); - return toApiJsonSerializer.serialize(result); - } else if (is(commandParam, "unassignSavingsOfficer")) { - final CommandWrapper commandRequest = builder.unassignSavingsOfficer(accountId).build(); - result = commandsSourceWritePlatformService.logCommandSource(commandRequest); - return toApiJsonSerializer.serialize(result); - } else if (is(commandParam, SavingsApiConstants.COMMAND_BLOCK_DEBIT)) { - final CommandWrapper commandRequest = builder.blockDebitsFromSavingsAccount(accountId).build(); - result = commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, SavingsApiConstants.COMMAND_UNBLOCK_DEBIT)) { - final CommandWrapper commandRequest = builder.unblockDebitsFromSavingsAccount(accountId).build(); - result = commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, SavingsApiConstants.COMMAND_BLOCK_CREDIT)) { - final CommandWrapper commandRequest = builder.blockCreditsToSavingsAccount(accountId).build(); - result = commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, SavingsApiConstants.COMMAND_UNBLOCK_CREDIT)) { - final CommandWrapper commandRequest = builder.unblockCreditsToSavingsAccount(accountId).build(); - result = commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, SavingsApiConstants.COMMAND_BLOCK_ACCOUNT)) { - final CommandWrapper commandRequest = builder.blockSavingsAccount(accountId).build(); - result = commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, SavingsApiConstants.COMMAND_UNBLOCK_ACCOUNT)) { - final CommandWrapper commandRequest = builder.unblockSavingsAccount(accountId).build(); - result = commandsSourceWritePlatformService.logCommandSource(commandRequest); - } - - if (result == null) { - // - throw new UnrecognizedQueryParamException("command", commandParam, - new Object[] { "reject", "withdrawnByApplicant", "approve", "undoapproval", "activate", "calculateInterest", - "postInterest", "close", "assignSavingsOfficer", "unassignSavingsOfficer", - SavingsApiConstants.COMMAND_BLOCK_DEBIT, SavingsApiConstants.COMMAND_UNBLOCK_DEBIT, - SavingsApiConstants.COMMAND_BLOCK_CREDIT, SavingsApiConstants.COMMAND_UNBLOCK_CREDIT, - SavingsApiConstants.COMMAND_BLOCK_ACCOUNT, SavingsApiConstants.COMMAND_UNBLOCK_ACCOUNT }); - } - - return toApiJsonSerializer.serialize(result); - } - - private String deleteSavingAccount(Long accountId, String externalId) { - ExternalId accountExternalId = ExternalIdFactory.produce(externalId); - accountId = getResolvedAccountId(accountId, accountExternalId); - - final CommandWrapper commandRequest = new CommandWrapperBuilder().deleteSavingsAccount(accountId).build(); - - final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest); - - return toApiJsonSerializer.serialize(result); - } - - private Long getResolvedAccountId(Long accountId, ExternalId accountExternalId) { - Long resolvedAccountId = accountId; - if (resolvedAccountId == null) { - accountExternalId.throwExceptionIfEmpty(); - resolvedAccountId = savingsAccountReadPlatformService.retrieveAccountIdByExternalId(accountExternalId); - if (resolvedAccountId == null) { - throw new SavingsAccountNotFoundException(resolvedAccountId); - } - } - return resolvedAccountId; - } - - private SavingsAccountData populateTemplateAndAssociations(final Long accountId, final SavingsAccountData savingsAccount, - final boolean staffInSelectedOfficeOnly, final String chargeStatus, final UriInfo uriInfo) { - - Collection transactions = null; - Collection charges = null; - - final Set associationParameters = ApiParameterHelper.extractAssociationsForResponseIfProvided(uriInfo.getQueryParameters()); - if (!associationParameters.isEmpty()) { - - if (associationParameters.contains("all")) { - associationParameters.addAll(Arrays.asList(SavingsApiConstants.transactions, SavingsApiConstants.charges)); - } - - if (associationParameters.contains(SavingsApiConstants.transactions)) { - final Collection currentTransactions = savingsAccountReadPlatformService - .retrieveAllTransactions(accountId, DepositAccountType.SAVINGS_DEPOSIT); - if (!CollectionUtils.isEmpty(currentTransactions)) { - transactions = currentTransactions; - } - } - - if (associationParameters.contains(SavingsApiConstants.charges)) { - final Collection currentCharges = savingsAccountChargeReadPlatformService - .retrieveSavingsAccountCharges(accountId, chargeStatus); - if (!CollectionUtils.isEmpty(currentCharges)) { - charges = currentCharges; - } - } - } - - SavingsAccountData templateData = null; - final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters()); - if (settings.isTemplate()) { - templateData = savingsAccountTemplateReadPlatformService.retrieveTemplate(savingsAccount.getClientId(), - savingsAccount.getGroupId(), savingsAccount.getSavingsProductId(), staffInSelectedOfficeOnly); - } - - return SavingsAccountData.withTemplateOptions(savingsAccount, templateData, transactions, charges); - } -} +/** + * 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.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.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.DefaultValue; +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.Response; +import jakarta.ws.rs.core.UriInfo; +import java.io.InputStream; +import java.util.Arrays; +import java.util.Collection; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +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.bulkimport.data.GlobalEntityType; +import org.apache.fineract.infrastructure.bulkimport.service.BulkImportWorkbookPopulatorService; +import org.apache.fineract.infrastructure.bulkimport.service.BulkImportWorkbookService; +import org.apache.fineract.infrastructure.core.api.ApiParameterHelper; +import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.data.UploadRequest; +import org.apache.fineract.infrastructure.core.domain.ExternalId; +import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException; +import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings; +import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; +import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; +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.savings.DepositAccountType; +import org.apache.fineract.portfolio.savings.SavingsApiConstants; +import org.apache.fineract.portfolio.savings.data.SavingsAccountChargeData; +import org.apache.fineract.portfolio.savings.data.SavingsAccountData; +import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData; +import org.apache.fineract.portfolio.savings.exception.SavingsAccountNotFoundException; +import org.apache.fineract.portfolio.savings.service.SavingsAccountChargeReadPlatformService; +import org.apache.fineract.portfolio.savings.service.SavingsAccountReadPlatformService; +import org.apache.fineract.portfolio.savings.service.SavingsAccountTemplateReadPlatformService; +import org.glassfish.jersey.media.multipart.FormDataContentDisposition; +import org.glassfish.jersey.media.multipart.FormDataParam; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; + +@Path("/v1/savingsaccounts") +@Component +@Tag(name = "Savings Account", description = "Savings accounts are instances of a particular savings product created for an individual or group. An application process around the creation of accounts is also supported.") +@RequiredArgsConstructor +public class SavingsAccountsApiResource { + + private final SavingsAccountReadPlatformService savingsAccountReadPlatformService; + private final SavingsAccountTemplateReadPlatformService savingsAccountTemplateReadPlatformService; + private final PlatformSecurityContext context; + private final DefaultToApiJsonSerializer toApiJsonSerializer; + private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; + private final ApiRequestParameterHelper apiRequestParameterHelper; + private final SavingsAccountChargeReadPlatformService savingsAccountChargeReadPlatformService; + private final BulkImportWorkbookService bulkImportWorkbookService; + private final BulkImportWorkbookPopulatorService bulkImportWorkbookPopulatorService; + private final SqlValidator sqlValidator; + + @GET + @Path("template") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Retrieve Savings Account 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\n" + "Example Requests:\n" + "\n" + "savingsaccounts/template?clientId=1\n" + + "\n" + "\n" + "savingsaccounts/template?clientId=1&productId=1") + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = SavingsAccountsApiResourceSwagger.GetSavingsAccountsTemplateResponse.class))) + public String template(@QueryParam("clientId") @Parameter(description = "clientId") final Long clientId, + @QueryParam("groupId") @Parameter(description = "groupId") final Long groupId, + @QueryParam("productId") @Parameter(description = "productId") final Long productId, + @DefaultValue("false") @QueryParam("staffInSelectedOfficeOnly") @Parameter(description = "staffInSelectedOfficeOnly") final boolean staffInSelectedOfficeOnly, + @Context final UriInfo uriInfo) { + + context.authenticatedUser().validateHasReadPermission(SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME); + + final SavingsAccountData savingsAccount = savingsAccountTemplateReadPlatformService.retrieveTemplate(clientId, groupId, productId, + staffInSelectedOfficeOnly); + + final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + return toApiJsonSerializer.serialize(settings, savingsAccount, SavingsApiSetConstants.SAVINGS_ACCOUNT_RESPONSE_DATA_PARAMETERS); + } + + @GET + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "List savings applications/accounts", description = "Lists savings applications/accounts\n\n" + + "Example Requests:\n" + "\n" + "savingsaccounts\n" + "\n" + "\n" + "savingsaccounts?fields=name") + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = SavingsAccountsApiResourceSwagger.GetSavingsAccountsResponse.class))) + public String retrieveAll(@Context final UriInfo uriInfo, + @QueryParam("externalId") @Parameter(description = "externalId") final String externalId, + // @QueryParam("underHierarchy") final String hierarchy, + @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("accountNo") @Parameter(description = "accountNo") final String accountNo, + @QueryParam("clientName") @Parameter(description = "clientName") final String clientName, + @QueryParam("depositType") @Parameter(description = "depositType") final Integer depositType) { + + context.authenticatedUser().validateHasReadPermission(SavingsApiConstants.SAVINGS_ACCOUNT_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).accountNo(accountNo).name(clientName).depositType(depositType).build(); + + final Page products = savingsAccountReadPlatformService.retrieveAll(searchParameters); + + final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + return toApiJsonSerializer.serialize(settings, products, SavingsApiSetConstants.SAVINGS_ACCOUNT_RESPONSE_DATA_PARAMETERS); + } + + @POST + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Submit new savings application", description = "Submits new savings application\n\n" + + "Mandatory Fields: clientId or groupId, productId, submittedOnDate\n\n" + + "Optional Fields: accountNo, externalId, fieldOfficerId\n\n" + + "Inherited from Product (if not provided): nominalAnnualInterestRate, interestCompoundingPeriodType, interestCalculationType, interestCalculationDaysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType, withdrawalFeeForTransfers, allowOverdraft, overdraftLimit, withHoldTax\n\n" + + "Additional Mandatory Field if Entity-Datatable Check is enabled for the entity of type Savings: datatables") + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = SavingsAccountsApiResourceSwagger.PostSavingsAccountsRequest.class))) + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = SavingsAccountsApiResourceSwagger.PostSavingsAccountsResponse.class))) + public String submitApplication(@Parameter(hidden = true) final String apiRequestBodyAsJson) { + + final CommandWrapper commandRequest = new CommandWrapperBuilder().createSavingsAccount().withJson(apiRequestBodyAsJson).build(); + + final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest); + + return toApiJsonSerializer.serialize(result); + } + + @POST + @Path("/gsim") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String submitGSIMApplication(final String apiRequestBodyAsJson) { + + final CommandWrapper commandRequest = new CommandWrapperBuilder().createGSIMAccount().withJson(apiRequestBodyAsJson).build(); + + final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest); + + return toApiJsonSerializer.serialize(result); + } + + @GET + @Path("{accountId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public SavingsAccountData retrieveOne(@PathParam("accountId") final Long accountId, + @DefaultValue("false") @QueryParam("staffInSelectedOfficeOnly") final boolean staffInSelectedOfficeOnly, + @DefaultValue("all") @QueryParam("chargeStatus") final String chargeStatus, + @QueryParam("associations") final String associations, @Context final UriInfo uriInfo) { + + return retrieveSavingAccount(accountId, null, staffInSelectedOfficeOnly, chargeStatus, uriInfo); + } + + @GET + @Path("/external-id/{externalId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public SavingsAccountData retrieveOne(@PathParam("externalId") final String externalId, + @DefaultValue("false") @QueryParam("staffInSelectedOfficeOnly") final boolean staffInSelectedOfficeOnly, + @DefaultValue("all") @QueryParam("chargeStatus") final String chargeStatus, + @QueryParam("associations") final String associations, @Context final UriInfo uriInfo) { + + return retrieveSavingAccount(null, externalId, staffInSelectedOfficeOnly, chargeStatus, uriInfo); + } + + @PUT + @Path("{accountId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Modify a savings application | Modify savings account withhold tax applicability", description = "Modify a savings application:\n\n" + + "Savings application can only be modified when in 'Submitted and pending approval' state. Once the application is approved, the details cannot be changed using this method. Specific api endpoints will be created to allow change of interest detail such as rate, compounding period, posting period etc\n\n" + + "Modify savings account withhold tax applicability:\n\n" + + "Savings application's withhold tax can be modified when in 'Active' state. Once the application is activated, can modify the account withhold tax to post tax or vice-versa" + + "Showing request/response for 'Modify a savings application'") + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = SavingsAccountsApiResourceSwagger.PutSavingsAccountsAccountIdRequest.class))) + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = SavingsAccountsApiResourceSwagger.PutSavingsAccountsAccountIdResponse.class))) + public String update(@PathParam("accountId") @Parameter(description = "accountId") final Long accountId, + @Parameter(hidden = true) final String apiRequestBodyAsJson, + @QueryParam("command") @Parameter(description = "command") final String commandParam) { + + return updateSavingAccount(accountId, null, apiRequestBodyAsJson, commandParam); + } + + @PUT + @Path("/external-id/{externalId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Modify a savings application | Modify savings account withhold tax applicability", description = "Modify a savings application:\n\n" + + "Savings application can only be modified when in 'Submitted and pending approval' state. Once the application is approved, the details cannot be changed using this method. Specific api endpoints will be created to allow change of interest detail such as rate, compounding period, posting period etc\n\n" + + "Modify savings account withhold tax applicability:\n\n" + + "Savings application's withhold tax can be modified when in 'Active' state. Once the application is activated, can modify the account withhold tax to post tax or vice-versa" + + "Showing request/response for 'Modify a savings application'") + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = SavingsAccountsApiResourceSwagger.PutSavingsAccountsAccountIdRequest.class))) + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = SavingsAccountsApiResourceSwagger.PutSavingsAccountsAccountIdResponse.class))) + public String update(@PathParam("externalId") @Parameter(description = "externalId") final String externalId, + @Parameter(hidden = true) final String apiRequestBodyAsJson, + @QueryParam("command") @Parameter(description = "command") final String commandParam) { + + return updateSavingAccount(null, externalId, apiRequestBodyAsJson, commandParam); + } + + @PUT + @Path("/gsim/{parentAccountId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String updateGsim(@PathParam("parentAccountId") final Long parentAccountId, final String apiRequestBodyAsJson) { + final CommandWrapper commandRequest = new CommandWrapperBuilder().updateGSIMAccount(parentAccountId).withJson(apiRequestBodyAsJson) + .build(); + + final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest); + + return toApiJsonSerializer.serialize(result); + } + + @POST + @Path("/gsimcommands/{parentAccountId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String handleGSIMCommands(@PathParam("parentAccountId") final Long parentAccountId, + @QueryParam("command") final String commandParam, final String apiRequestBodyAsJson) { + + String jsonApiRequest = apiRequestBodyAsJson; + if (StringUtils.isBlank(jsonApiRequest)) { + jsonApiRequest = "{}"; + } + + final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(jsonApiRequest); + + CommandProcessingResult result = null; + if (is(commandParam, "reject")) { + final CommandWrapper commandRequest = builder.rejectGSIMAccountApplication(parentAccountId).build(); + result = commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (is(commandParam, "withdrawnByApplicant")) { + final CommandWrapper commandRequest = builder.withdrawSavingsAccountApplication(parentAccountId).build(); + result = commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (is(commandParam, "approve")) { + final CommandWrapper commandRequest = builder.approveGSIMAccountApplication(parentAccountId).build(); + result = commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (is(commandParam, "undoapproval")) { + final CommandWrapper commandRequest = builder.undoGSIMApplicationApproval(parentAccountId).build(); + result = commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (is(commandParam, "activate")) { + final CommandWrapper commandRequest = builder.gsimAccountActivation(parentAccountId).build(); + result = commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (is(commandParam, "calculateInterest")) { + final CommandWrapper commandRequest = builder.withNoJsonBody().savingsAccountInterestCalculation(parentAccountId).build(); + result = commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (is(commandParam, "postInterest")) { + final CommandWrapper commandRequest = builder.savingsAccountInterestPosting(parentAccountId).build(); + result = commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (is(commandParam, "applyAnnualFees")) { + final CommandWrapper commandRequest = builder.savingsAccountApplyAnnualFees(parentAccountId).build(); + result = commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (is(commandParam, "close")) { + final CommandWrapper commandRequest = builder.closeGSIMApplication(parentAccountId).build(); + result = commandsSourceWritePlatformService.logCommandSource(commandRequest); + } + if (result == null) { + throw new UnrecognizedQueryParamException("command", commandParam, + new Object[] { "reject", "withdrawnByApplicant", "approve", "undoapproval", "activate", "calculateInterest", + "postInterest", "close", "assignSavingsOfficer", "unassignSavingsOfficer", + SavingsApiConstants.COMMAND_BLOCK_DEBIT, SavingsApiConstants.COMMAND_UNBLOCK_DEBIT, + SavingsApiConstants.COMMAND_BLOCK_CREDIT, SavingsApiConstants.COMMAND_UNBLOCK_CREDIT, + SavingsApiConstants.COMMAND_BLOCK_ACCOUNT, SavingsApiConstants.COMMAND_UNBLOCK_ACCOUNT }); + } + + return toApiJsonSerializer.serialize(result); + } + + @POST + @Path("{accountId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Approve savings application | Undo approval savings application | Assign Savings Officer | Unassign Savings Officer | Reject savings application | Withdraw savings application | Activate a savings account | Close a savings account | Calculate Interest on Savings Account | Post Interest on Savings Account | Block Savings Account | Unblock Savings Account | Block Savings Account Credit transactions | Unblock Savings Account Credit transactions | Block Savings Account Debit transactions | Unblock Savings Account debit transactions", description = "Approve savings application:\n\n" + + "Approves savings application so long as its in 'Submitted and pending approval' state.\n\n" + + "Undo approval savings application:\n\n" + + "Will move 'approved' savings application back to 'Submitted and pending approval' state.\n\n" + "Assign Savings Officer:\n\n" + + "Allows you to assign Savings Officer for existing Savings Account.\n\n" + "Unassign Savings Officer:\n\n" + + "Allows you to unassign the Savings Officer.\n\n" + "Reject savings application:\n\n" + + "Rejects savings application so long as its in 'Submitted and pending approval' state.\n\n" + + "Withdraw savings application:\n\n" + + "Used when an applicant withdraws from the savings application. It must be in 'Submitted and pending approval' state.\n\n" + + "Activate a savings account:\n\n" + + "Results in an approved savings application being converted into an 'active' savings account.\n\n" + + "Close a savings account:\n\n" + + "Results in an Activated savings application being converted into an 'closed' savings account.\n" + "\n" + + "closedOnDate is closure date of savings account\n" + "\n" + + "withdrawBalance is a boolean description, true value of this field performs a withdrawal transaction with account's running balance.\n\n" + + "Mandatory Fields: dateFormat,locale,closedOnDate\n\n" + + "Optional Fields: note, withdrawBalance, paymentTypeId, accountNumber, checkNumber, routingCode, receiptNumber, bankNumber\n\n" + + "Calculate Interest on Savings Account:\n\n" + + "Calculates interest earned on a savings account based on todays date. It does not attempt to post or credit the interest on the account. That is responsibility of the Post Interest API that will likely be called by overnight process.\n\n" + + "Post Interest on Savings Account:\n\n" + + "Calculates and Posts interest earned on a savings account based on today's date and whether an interest posting or crediting event is due.\n\n" + + "Block Savings Account:\n\n" + "Blocks Savings account from all types of credit and debit transactions\n\n" + + "Unblock Savings Account:\n\n" + + "Unblock a blocked account. On unblocking account, user can perform debit and credit transactions\n\n" + + "Block Savings Account Credit transactions:\n\n" + + "Savings account will be blocked from all types of credit transactions.\n\n" + + "Unblock Savings Account Credit transactions:\n\n" + + "It unblocks the Saving account's credit operations. Now all types of credits can be transacted to Savings account\n\n" + + "Block Savings Account Debit transactions:\n\n" + "All types of debit operations from Savings account wil be blocked\n\n" + + "Unblock Savings Account debit transactions:\n\n" + + "It unblocks the Saving account's debit operations. Now all types of debits can be transacted from Savings account\n\n" + + "Showing request/response for 'Unassign Savings Officer'") + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = SavingsAccountsApiResourceSwagger.PostSavingsAccountsAccountIdRequest.class))) + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = SavingsAccountsApiResourceSwagger.PostSavingsAccountsAccountIdResponse.class))) + public String handleCommands(@PathParam("accountId") @Parameter(description = "accountId") final Long accountId, + @QueryParam("command") @Parameter(description = "command") final String commandParam, + @Parameter(hidden = true) final String apiRequestBodyAsJson) { + + return handleCommands(accountId, null, commandParam, apiRequestBodyAsJson); + } + + @POST + @Path("/external-id/{externalId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Approve savings application | Undo approval savings application | Assign Savings Officer | Unassign Savings Officer | Reject savings application | Withdraw savings application | Activate a savings account | Close a savings account | Calculate Interest on Savings Account | Post Interest on Savings Account | Block Savings Account | Unblock Savings Account | Block Savings Account Credit transactions | Unblock Savings Account Credit transactions | Block Savings Account Debit transactions | Unblock Savings Account debit transactions", description = "Approve savings application:\n\n" + + "Approves savings application so long as its in 'Submitted and pending approval' state.\n\n" + + "Undo approval savings application:\n\n" + + "Will move 'approved' savings application back to 'Submitted and pending approval' state.\n\n" + "Assign Savings Officer:\n\n" + + "Allows you to assign Savings Officer for existing Savings Account.\n\n" + "Unassign Savings Officer:\n\n" + + "Allows you to unassign the Savings Officer.\n\n" + "Reject savings application:\n\n" + + "Rejects savings application so long as its in 'Submitted and pending approval' state.\n\n" + + "Withdraw savings application:\n\n" + + "Used when an applicant withdraws from the savings application. It must be in 'Submitted and pending approval' state.\n\n" + + "Activate a savings account:\n\n" + + "Results in an approved savings application being converted into an 'active' savings account.\n\n" + + "Close a savings account:\n\n" + + "Results in an Activated savings application being converted into an 'closed' savings account.\n" + "\n" + + "closedOnDate is closure date of savings account\n" + "\n" + + "withdrawBalance is a boolean description, true value of this field performs a withdrawal transaction with account's running balance.\n\n" + + "Mandatory Fields: dateFormat,locale,closedOnDate\n\n" + + "Optional Fields: note, withdrawBalance, paymentTypeId, accountNumber, checkNumber, routingCode, receiptNumber, bankNumber\n\n" + + "Calculate Interest on Savings Account:\n\n" + + "Calculates interest earned on a savings account based on todays date. It does not attempt to post or credit the interest on the account. That is responsibility of the Post Interest API that will likely be called by overnight process.\n\n" + + "Post Interest on Savings Account:\n\n" + + "Calculates and Posts interest earned on a savings account based on today's date and whether an interest posting or crediting event is due.\n\n" + + "Block Savings Account:\n\n" + "Blocks Savings account from all types of credit and debit transactions\n\n" + + "Unblock Savings Account:\n\n" + + "Unblock a blocked account. On unblocking account, user can perform debit and credit transactions\n\n" + + "Block Savings Account Credit transactions:\n\n" + + "Savings account will be blocked from all types of credit transactions.\n\n" + + "Unblock Savings Account Credit transactions:\n\n" + + "It unblocks the Saving account's credit operations. Now all types of credits can be transacted to Savings account\n\n" + + "Block Savings Account Debit transactions:\n\n" + "All types of debit operations from Savings account wil be blocked\n\n" + + "Unblock Savings Account debit transactions:\n\n" + + "It unblocks the Saving account's debit operations. Now all types of debits can be transacted from Savings account\n\n" + + "Showing request/response for 'Unassign Savings Officer'") + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = SavingsAccountsApiResourceSwagger.PostSavingsAccountsAccountIdRequest.class))) + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = SavingsAccountsApiResourceSwagger.PostSavingsAccountsAccountIdResponse.class))) + public String handleCommands(@PathParam("externalId") @Parameter(description = "externalId") final String externalId, + @QueryParam("command") @Parameter(description = "command") final String commandParam, + @Parameter(hidden = true) final String apiRequestBodyAsJson) { + + return handleCommands(null, externalId, commandParam, apiRequestBodyAsJson); + } + + private boolean is(final String commandParam, final String commandValue) { + return StringUtils.isNotBlank(commandParam) && commandParam.trim().equalsIgnoreCase(commandValue); + } + + @DELETE + @Path("{accountId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Delete a savings application", description = "At present we support hard delete of savings application so long as its in 'Submitted and pending approval' state. One the application is moves past this state, it is not possible to do a 'hard' delete of the application or the account. An API endpoint will be added to close/de-activate the savings account.") + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = SavingsAccountsApiResourceSwagger.DeleteSavingsAccountsAccountIdResponse.class))) + public String delete(@PathParam("accountId") @Parameter(description = "accountId") final Long accountId) { + + return deleteSavingAccount(accountId, null); + } + + @DELETE + @Path("/external-id/{externalId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Delete a savings application", description = "At present we support hard delete of savings application so long as its in 'Submitted and pending approval' state. One the application is moves past this state, it is not possible to do a 'hard' delete of the application or the account. An API endpoint will be added to close/de-activate the savings account.") + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = SavingsAccountsApiResourceSwagger.DeleteSavingsAccountsAccountIdResponse.class))) + public String delete(@PathParam("externalId") @Parameter(description = "externalId") final String externalId) { + + return deleteSavingAccount(null, externalId); + } + + @GET + @Path("downloadtemplate") + @Produces("application/vnd.ms-excel") + public Response getSavingsTemplate(@QueryParam("officeId") final Long officeId, @QueryParam("staffId") final Long staffId, + @QueryParam("dateFormat") final String dateFormat) { + return bulkImportWorkbookPopulatorService.getTemplate(GlobalEntityType.SAVINGS_ACCOUNT.toString(), officeId, staffId, dateFormat); + } + + @POST + @Path("uploadtemplate") + @Consumes(MediaType.MULTIPART_FORM_DATA) + @RequestBody(description = "Upload savings template", content = { + @Content(mediaType = MediaType.MULTIPART_FORM_DATA, schema = @Schema(implementation = UploadRequest.class)) }) + public String postSavingsTemplate(@FormDataParam("file") InputStream uploadedInputStream, + @FormDataParam("file") FormDataContentDisposition fileDetail, @FormDataParam("locale") final String locale, + @FormDataParam("dateFormat") final String dateFormat) { + final Long importDocumentId = bulkImportWorkbookService.importWorkbook(GlobalEntityType.SAVINGS_ACCOUNT.toString(), + uploadedInputStream, fileDetail, locale, dateFormat); + return toApiJsonSerializer.serialize(importDocumentId); + } + + @GET + @Path("transactions/downloadtemplate") + @Produces("application/vnd.ms-excel") + public Response getSavingsTransactionTemplate(@QueryParam("officeId") final Long officeId, + @QueryParam("dateFormat") final String dateFormat) { + return bulkImportWorkbookPopulatorService.getTemplate(GlobalEntityType.SAVINGS_TRANSACTIONS.toString(), officeId, null, dateFormat); + } + + @POST + @Path("transactions/uploadtemplate") + @Consumes(MediaType.MULTIPART_FORM_DATA) + @RequestBody(description = "Upload savings transaction template", content = { + @Content(mediaType = MediaType.MULTIPART_FORM_DATA, schema = @Schema(implementation = UploadRequest.class)) }) + public String postSavingsTransactionTemplate(@FormDataParam("file") InputStream uploadedInputStream, + @FormDataParam("file") FormDataContentDisposition fileDetail, @FormDataParam("locale") final String locale, + @FormDataParam("dateFormat") final String dateFormat) { + final Long importDocumentId = bulkImportWorkbookService.importWorkbook(GlobalEntityType.SAVINGS_TRANSACTIONS.toString(), + uploadedInputStream, fileDetail, locale, dateFormat); + return toApiJsonSerializer.serialize(importDocumentId); + } + + private SavingsAccountData retrieveSavingAccount(Long accountId, String externalId, boolean staffInSelectedOfficeOnly, + String chargeStatus, UriInfo uriInfo) { + context.authenticatedUser().validateHasReadPermission(SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME); + + if (!(is(chargeStatus, "all") || is(chargeStatus, "active") || is(chargeStatus, "inactive"))) { + throw new UnrecognizedQueryParamException("status", chargeStatus, new Object[] { "all", "active", "inactive" }); + } + + ExternalId accountExternalId = ExternalIdFactory.produce(externalId); + accountId = getResolvedAccountId(accountId, accountExternalId); + final SavingsAccountData savingsAccount = savingsAccountReadPlatformService.retrieveOne(accountId); + + return populateTemplateAndAssociations(accountId, savingsAccount, staffInSelectedOfficeOnly, chargeStatus, uriInfo); + } + + private String updateSavingAccount(Long accountId, String externalId, String apiRequestBodyAsJson, String commandParam) { + ExternalId accountExternalId = ExternalIdFactory.produce(externalId); + accountId = getResolvedAccountId(accountId, accountExternalId); + + if (is(commandParam, "updateWithHoldTax")) { + final CommandWrapper commandRequest = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson).updateWithHoldTax(accountId) + .build(); + final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest); + return toApiJsonSerializer.serialize(result); + } + + final CommandWrapper commandRequest = new CommandWrapperBuilder().updateSavingsAccount(accountId).withJson(apiRequestBodyAsJson) + .build(); + + final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest); + + return toApiJsonSerializer.serialize(result); + } + + private String handleCommands(Long accountId, String externalId, String commandParam, String apiRequestBodyAsJson) { + ExternalId accountExternalId = ExternalIdFactory.produce(externalId); + accountId = getResolvedAccountId(accountId, accountExternalId); + + String jsonApiRequest = apiRequestBodyAsJson; + if (StringUtils.isBlank(jsonApiRequest)) { + jsonApiRequest = "{}"; + } + + final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(jsonApiRequest); + + CommandProcessingResult result = null; + if (is(commandParam, "reject")) { + final CommandWrapper commandRequest = builder.rejectSavingsAccountApplication(accountId).build(); + result = commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (is(commandParam, "withdrawnByApplicant")) { + final CommandWrapper commandRequest = builder.withdrawSavingsAccountApplication(accountId).build(); + result = commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (is(commandParam, "approve")) { + final CommandWrapper commandRequest = builder.approveSavingsAccountApplication(accountId).build(); + result = commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (is(commandParam, "undoapproval")) { + final CommandWrapper commandRequest = builder.undoSavingsAccountApplication(accountId).build(); + result = commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (is(commandParam, "activate")) { + final CommandWrapper commandRequest = builder.savingsAccountActivation(accountId).build(); + result = commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (is(commandParam, "calculateInterest")) { + final CommandWrapper commandRequest = builder.withNoJsonBody().savingsAccountInterestCalculation(accountId).build(); + result = commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (is(commandParam, "postInterest")) { + final CommandWrapper commandRequest = builder.savingsAccountInterestPosting(accountId).build(); + result = commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (is(commandParam, "applyAnnualFees")) { + final CommandWrapper commandRequest = builder.savingsAccountApplyAnnualFees(accountId).build(); + result = commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (is(commandParam, "close")) { + final CommandWrapper commandRequest = builder.closeSavingsAccountApplication(accountId).build(); + result = commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (is(commandParam, "assignSavingsOfficer")) { + final CommandWrapper commandRequest = builder.assignSavingsOfficer(accountId).build(); + result = commandsSourceWritePlatformService.logCommandSource(commandRequest); + return toApiJsonSerializer.serialize(result); + } else if (is(commandParam, "unassignSavingsOfficer")) { + final CommandWrapper commandRequest = builder.unassignSavingsOfficer(accountId).build(); + result = commandsSourceWritePlatformService.logCommandSource(commandRequest); + return toApiJsonSerializer.serialize(result); + } else if (is(commandParam, SavingsApiConstants.COMMAND_BLOCK_DEBIT)) { + final CommandWrapper commandRequest = builder.blockDebitsFromSavingsAccount(accountId).build(); + result = commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (is(commandParam, SavingsApiConstants.COMMAND_UNBLOCK_DEBIT)) { + final CommandWrapper commandRequest = builder.unblockDebitsFromSavingsAccount(accountId).build(); + result = commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (is(commandParam, SavingsApiConstants.COMMAND_BLOCK_CREDIT)) { + final CommandWrapper commandRequest = builder.blockCreditsToSavingsAccount(accountId).build(); + result = commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (is(commandParam, SavingsApiConstants.COMMAND_UNBLOCK_CREDIT)) { + final CommandWrapper commandRequest = builder.unblockCreditsToSavingsAccount(accountId).build(); + result = commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (is(commandParam, SavingsApiConstants.COMMAND_BLOCK_ACCOUNT)) { + final CommandWrapper commandRequest = builder.blockSavingsAccount(accountId).build(); + result = commandsSourceWritePlatformService.logCommandSource(commandRequest); + } else if (is(commandParam, SavingsApiConstants.COMMAND_UNBLOCK_ACCOUNT)) { + final CommandWrapper commandRequest = builder.unblockSavingsAccount(accountId).build(); + result = commandsSourceWritePlatformService.logCommandSource(commandRequest); + } + + if (result == null) { + // + throw new UnrecognizedQueryParamException("command", commandParam, + new Object[] { "reject", "withdrawnByApplicant", "approve", "undoapproval", "activate", "calculateInterest", + "postInterest", "close", "assignSavingsOfficer", "unassignSavingsOfficer", + SavingsApiConstants.COMMAND_BLOCK_DEBIT, SavingsApiConstants.COMMAND_UNBLOCK_DEBIT, + SavingsApiConstants.COMMAND_BLOCK_CREDIT, SavingsApiConstants.COMMAND_UNBLOCK_CREDIT, + SavingsApiConstants.COMMAND_BLOCK_ACCOUNT, SavingsApiConstants.COMMAND_UNBLOCK_ACCOUNT }); + } + + return toApiJsonSerializer.serialize(result); + } + + private String deleteSavingAccount(Long accountId, String externalId) { + ExternalId accountExternalId = ExternalIdFactory.produce(externalId); + accountId = getResolvedAccountId(accountId, accountExternalId); + + final CommandWrapper commandRequest = new CommandWrapperBuilder().deleteSavingsAccount(accountId).build(); + + final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest); + + return toApiJsonSerializer.serialize(result); + } + + private Long getResolvedAccountId(Long accountId, ExternalId accountExternalId) { + Long resolvedAccountId = accountId; + if (resolvedAccountId == null) { + accountExternalId.throwExceptionIfEmpty(); + resolvedAccountId = savingsAccountReadPlatformService.retrieveAccountIdByExternalId(accountExternalId); + if (resolvedAccountId == null) { + throw new SavingsAccountNotFoundException(resolvedAccountId); + } + } + return resolvedAccountId; + } + + private SavingsAccountData populateTemplateAndAssociations(final Long accountId, final SavingsAccountData savingsAccount, + final boolean staffInSelectedOfficeOnly, final String chargeStatus, final UriInfo uriInfo) { + + Collection transactions = null; + Collection charges = null; + + final Set associationParameters = ApiParameterHelper.extractAssociationsForResponseIfProvided(uriInfo.getQueryParameters()); + if (!associationParameters.isEmpty()) { + + if (associationParameters.contains("all")) { + associationParameters.addAll(Arrays.asList(SavingsApiConstants.transactions, SavingsApiConstants.charges)); + } + + if (associationParameters.contains(SavingsApiConstants.transactions)) { + final Collection currentTransactions = savingsAccountReadPlatformService + .retrieveAllTransactions(accountId, DepositAccountType.SAVINGS_DEPOSIT); + if (!CollectionUtils.isEmpty(currentTransactions)) { + transactions = currentTransactions; + } + } + + if (associationParameters.contains(SavingsApiConstants.charges)) { + final Collection currentCharges = savingsAccountChargeReadPlatformService + .retrieveSavingsAccountCharges(accountId, chargeStatus); + if (!CollectionUtils.isEmpty(currentCharges)) { + charges = currentCharges; + } + } + } + + SavingsAccountData templateData = null; + final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + if (settings.isTemplate()) { + templateData = savingsAccountTemplateReadPlatformService.retrieveTemplate(savingsAccount.getClientId(), + savingsAccount.getGroupId(), savingsAccount.getSavingsProductId(), staffInSelectedOfficeOnly); + } + + return SavingsAccountData.withTemplateOptions(savingsAccount, templateData, transactions, charges); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/handler/current/CurrentAccountTransactionAdjustmentCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/handler/current/CurrentAccountTransactionAdjustmentCommandHandler.java index d1af50d6e..8cf592984 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/handler/current/CurrentAccountTransactionAdjustmentCommandHandler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/handler/current/CurrentAccountTransactionAdjustmentCommandHandler.java @@ -22,7 +22,7 @@ import org.apache.fineract.commands.annotation.CommandType; import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; -import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService; +import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountTransactionWritePlatformService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -31,10 +31,10 @@ import org.springframework.transaction.annotation.Transactional; @CommandType(entity = "CURRENTACCOUNT", action = "ADJUSTTRANSACTION") public class CurrentAccountTransactionAdjustmentCommandHandler implements NewCommandSourceHandler { - private final SavingsAccountWritePlatformService writePlatformService; + private final CurrentAccountTransactionWritePlatformService writePlatformService; @Autowired - public CurrentAccountTransactionAdjustmentCommandHandler(final SavingsAccountWritePlatformService writePlatformService) { + public CurrentAccountTransactionAdjustmentCommandHandler(final CurrentAccountTransactionWritePlatformService writePlatformService) { this.writePlatformService = writePlatformService; } @@ -42,6 +42,6 @@ public class CurrentAccountTransactionAdjustmentCommandHandler implements NewCom @Override public CommandProcessingResult processCommand(final JsonCommand command) { final Long transactionId = Long.valueOf(command.getTransactionId()); - return this.writePlatformService.adjustSavingsTransaction(command.getSavingsId(), transactionId, command); + return this.writePlatformService.adjustTransaction(command.getSavingsId(), transactionId, command); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/handler/current/DepositCurrentAccountCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/handler/current/DepositCurrentAccountCommandHandler.java index c331667ac..c87cefbcc 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/handler/current/DepositCurrentAccountCommandHandler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/handler/current/DepositCurrentAccountCommandHandler.java @@ -22,7 +22,7 @@ import org.apache.fineract.commands.annotation.CommandType; import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; -import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService; +import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountTransactionWritePlatformService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -31,10 +31,10 @@ import org.springframework.transaction.annotation.Transactional; @CommandType(entity = "CURRENTACCOUNT", action = "DEPOSIT") public class DepositCurrentAccountCommandHandler implements NewCommandSourceHandler { - private final SavingsAccountWritePlatformService writePlatformService; + private final CurrentAccountTransactionWritePlatformService writePlatformService; @Autowired - public DepositCurrentAccountCommandHandler(final SavingsAccountWritePlatformService writePlatformService) { + public DepositCurrentAccountCommandHandler(final CurrentAccountTransactionWritePlatformService writePlatformService) { this.writePlatformService = writePlatformService; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/handler/current/HoldAmountCurrentAccountCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/handler/current/HoldAmountCurrentAccountCommandHandler.java index ee3dc3fbd..fe5ff9630 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/handler/current/HoldAmountCurrentAccountCommandHandler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/handler/current/HoldAmountCurrentAccountCommandHandler.java @@ -22,7 +22,7 @@ import org.apache.fineract.commands.annotation.CommandType; import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; -import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService; +import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountTransactionWritePlatformService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -31,10 +31,10 @@ import org.springframework.transaction.annotation.Transactional; @CommandType(entity = "CURRENTACCOUNT", action = "HOLDAMOUNT") public class HoldAmountCurrentAccountCommandHandler implements NewCommandSourceHandler { - private final SavingsAccountWritePlatformService writePlatformService; + private final CurrentAccountTransactionWritePlatformService writePlatformService; @Autowired - public HoldAmountCurrentAccountCommandHandler(final SavingsAccountWritePlatformService writePlatformService) { + public HoldAmountCurrentAccountCommandHandler(final CurrentAccountTransactionWritePlatformService writePlatformService) { this.writePlatformService = writePlatformService; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/handler/current/ReleaseAmountCurrentAccountCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/handler/current/ReleaseAmountCurrentAccountCommandHandler.java index aa42e6b5e..bee360745 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/handler/current/ReleaseAmountCurrentAccountCommandHandler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/handler/current/ReleaseAmountCurrentAccountCommandHandler.java @@ -22,7 +22,7 @@ import org.apache.fineract.commands.annotation.CommandType; import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; -import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService; +import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountTransactionWritePlatformService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -31,10 +31,10 @@ import org.springframework.transaction.annotation.Transactional; @CommandType(entity = "CURRENTACCOUNT", action = "RELEASEAMOUNT") public class ReleaseAmountCurrentAccountCommandHandler implements NewCommandSourceHandler { - private final SavingsAccountWritePlatformService writePlatformService; + private final CurrentAccountTransactionWritePlatformService writePlatformService; @Autowired - public ReleaseAmountCurrentAccountCommandHandler(final SavingsAccountWritePlatformService writePlatformService) { + public ReleaseAmountCurrentAccountCommandHandler(final CurrentAccountTransactionWritePlatformService writePlatformService) { this.writePlatformService = writePlatformService; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/handler/current/ReverseTransactionCurrentAccountCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/handler/current/ReverseTransactionCurrentAccountCommandHandler.java index 12561459d..dcad329dd 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/handler/current/ReverseTransactionCurrentAccountCommandHandler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/handler/current/ReverseTransactionCurrentAccountCommandHandler.java @@ -22,7 +22,7 @@ import org.apache.fineract.commands.annotation.CommandType; import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; -import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService; +import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountTransactionWritePlatformService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -31,10 +31,10 @@ import org.springframework.transaction.annotation.Transactional; @CommandType(entity = "CURRENTACCOUNT", action = "REVERSETRANSACTION") public class ReverseTransactionCurrentAccountCommandHandler implements NewCommandSourceHandler { - private final SavingsAccountWritePlatformService writePlatformService; + private final CurrentAccountTransactionWritePlatformService writePlatformService; @Autowired - public ReverseTransactionCurrentAccountCommandHandler(final SavingsAccountWritePlatformService writePlatformService) { + public ReverseTransactionCurrentAccountCommandHandler(final CurrentAccountTransactionWritePlatformService writePlatformService) { this.writePlatformService = writePlatformService; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/handler/current/UndoTransactionCurrentAccountCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/handler/current/UndoTransactionCurrentAccountCommandHandler.java index 80cd9d3f8..25b5e49bd 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/handler/current/UndoTransactionCurrentAccountCommandHandler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/handler/current/UndoTransactionCurrentAccountCommandHandler.java @@ -22,7 +22,7 @@ import org.apache.fineract.commands.annotation.CommandType; import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; -import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService; +import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountTransactionWritePlatformService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -31,10 +31,10 @@ import org.springframework.transaction.annotation.Transactional; @CommandType(entity = "CURRENTACCOUNT", action = "UNDOTRANSACTION") public class UndoTransactionCurrentAccountCommandHandler implements NewCommandSourceHandler { - private final SavingsAccountWritePlatformService writePlatformService; + private final CurrentAccountTransactionWritePlatformService writePlatformService; @Autowired - public UndoTransactionCurrentAccountCommandHandler(final SavingsAccountWritePlatformService writePlatformService) { + public UndoTransactionCurrentAccountCommandHandler(final CurrentAccountTransactionWritePlatformService writePlatformService) { this.writePlatformService = writePlatformService; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/handler/current/WithdrawCurrentAccountCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/handler/current/WithdrawCurrentAccountCommandHandler.java index 5609c58f1..6245561d6 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/handler/current/WithdrawCurrentAccountCommandHandler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/handler/current/WithdrawCurrentAccountCommandHandler.java @@ -22,7 +22,7 @@ import org.apache.fineract.commands.annotation.CommandType; import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; -import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService; +import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountTransactionWritePlatformService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -31,10 +31,10 @@ import org.springframework.transaction.annotation.Transactional; @CommandType(entity = "CURRENTACCOUNT", action = "WITHDRAWAL") public class WithdrawCurrentAccountCommandHandler implements NewCommandSourceHandler { - private final SavingsAccountWritePlatformService writePlatformService; + private final CurrentAccountTransactionWritePlatformService writePlatformService; @Autowired - public WithdrawCurrentAccountCommandHandler(final SavingsAccountWritePlatformService writePlatformService) { + public WithdrawCurrentAccountCommandHandler(final CurrentAccountTransactionWritePlatformService writePlatformService) { this.writePlatformService = writePlatformService; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/CurrentAccountReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/CurrentAccountReadPlatformServiceImpl.java index 18b530551..318875b1d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/CurrentAccountReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/CurrentAccountReadPlatformServiceImpl.java @@ -1,533 +1,573 @@ -/** - * 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.sql.ResultSet; -import java.sql.ResultSetMetaData; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import lombok.RequiredArgsConstructor; -import org.apache.fineract.portfolio.currentaccount.exception.CurrentAccountNotFoundException; -import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountReadPlatformService; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.RowMapper; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class CurrentAccountReadPlatformServiceImpl implements CurrentAccountReadPlatformService { - - private static final String BASE_SQL = - "SELECT ca.*, " - + "c.display_name AS client_name, c.account_no AS client_account_no, " - + "o.name AS office_name, " - + "s.display_name AS staff_name, " - + "cp.name AS product_name, cp.short_name AS product_short_name, " - + "curr.name AS currency_name, curr.display_symbol AS currency_display_symbol, " - + "curr.internationalized_name_code AS currency_name_code " - + "FROM m_current_account ca " - + "LEFT JOIN m_client c ON c.id = ca.client_id " - + "LEFT JOIN m_office o ON o.id = ca.office_id " - + "LEFT JOIN m_staff s ON s.id = ca.staff_id " - + "LEFT JOIN m_current_product cp ON cp.id = ca.product_id " - + "LEFT JOIN m_currency curr ON curr.code = ca.currency_code "; - - private final JdbcTemplate jdbcTemplate; - - @Override - public Map retrieveOne(Long accountId) { - List> rows = jdbcTemplate.query( - BASE_SQL + "WHERE ca.id = ?", - new FlatRowMapper(), - accountId); - if (rows.isEmpty()) { - throw new CurrentAccountNotFoundException(accountId); - } - final Map response = buildStructuredResponse(rows.get(0)); - response.put("charges", retrieveCharges(accountId)); - response.put("transactions", retrieveTransactions(accountId)); - return response; - } - - private List> retrieveTransactions(Long accountId) { - final String sql = - "SELECT t.id, t.transaction_type_enum, t.transaction_date, t.amount, " - + "t.running_balance_derived, t.is_reversed, t.is_reversal, t.ref_no, t.remarks, " - + "t.submitted_on_date " - + "FROM m_current_account_transaction t " - + "WHERE t.current_account_id = ? " - + "ORDER BY t.transaction_date ASC, t.id ASC"; - - return jdbcTemplate.query(sql, (rs, rowNum) -> { - final Map t = new LinkedHashMap<>(); - t.put("id", rs.getLong("id")); - - final int txnTypeEnum = rs.getInt("transaction_type_enum"); - t.put("transactionType", buildTransactionType(txnTypeEnum)); - - final java.time.LocalDate txnDate = rs.getObject("transaction_date", java.time.LocalDate.class); - if (txnDate != null) { - t.put("date", new int[]{txnDate.getYear(), txnDate.getMonthValue(), txnDate.getDayOfMonth()}); - } else { - t.put("date", null); - } - - t.put("amount", rs.getBigDecimal("amount")); - t.put("runningBalance", rs.getBigDecimal("running_balance_derived")); - t.put("reversed", rs.getBoolean("is_reversed")); - t.put("isReversal", rs.getBoolean("is_reversal")); - t.put("refNo", rs.getString("ref_no")); - t.put("remarks", rs.getString("remarks")); - - final java.time.LocalDate submittedOn = rs.getObject("submitted_on_date", java.time.LocalDate.class); - if (submittedOn != null) { - t.put("submittedOnDate", new int[]{submittedOn.getYear(), submittedOn.getMonthValue(), submittedOn.getDayOfMonth()}); - } else { - t.put("submittedOnDate", null); - } - return t; - }, accountId); - } - - private Map buildTransactionType(int typeEnum) { - final Map tt = new LinkedHashMap<>(); - tt.put("id", typeEnum); - tt.put("deposit", typeEnum == 1); - tt.put("withdrawal", typeEnum == 2); - tt.put("interestPosting", typeEnum == 3); - tt.put("feeDeduction", typeEnum == 4 || typeEnum == 7); - tt.put("overdraftInterest", false); - tt.put("withholdTax", false); - tt.put("initiateTransfer", false); - tt.put("approveTransfer", false); - tt.put("withdrawTransfer", false); - tt.put("rejectTransfer", false); - tt.put("writtenoff", false); - tt.put("overdraftFee", false); - tt.put("escheat", false); - tt.put("amountHold", false); - tt.put("amountRelease", false); - tt.put("accrual", false); - switch (typeEnum) { - case 1 -> { tt.put("code", "savingsTransactionType.deposit"); tt.put("value", "Deposit"); } - case 2 -> { tt.put("code", "savingsTransactionType.withdrawal"); tt.put("value", "Withdrawal"); } - case 3 -> { tt.put("code", "savingsTransactionType.interestPosting"); tt.put("value", "Interest Posting"); } - case 4 -> { tt.put("code", "savingsTransactionType.feeDeduction"); tt.put("value", "Fee Deduction"); } - case 7 -> { tt.put("code", "savingsTransactionType.payCharge"); tt.put("value", "Charge Deduction"); } - default -> { tt.put("code", "savingsTransactionType.deposit"); tt.put("value", "Deposit"); } - } - return tt; - } - - private List> retrieveCharges(Long accountId) { - final String sql = - "SELECT cac.id, cac.charge_id, ch.name AS charge_name, " - + "cac.charge_time_enum, cac.charge_due_date, cac.fee_on_month, cac.fee_on_day, " - + "cac.fee_interval, cac.charge_calculation_enum, " - + "cac.amount, cac.amount_paid_derived, cac.amount_waived_derived, " - + "cac.amount_writtenoff_derived, cac.amount_outstanding_derived, " - + "cac.is_paid_derived, cac.waived, cac.is_penalty, cac.is_active " - + "FROM m_current_account_charge cac " - + "JOIN m_charge ch ON ch.id = cac.charge_id " - + "WHERE cac.current_account_id = ? AND cac.is_active = true " - + "ORDER BY cac.id"; - - return jdbcTemplate.query(sql, (rs, rowNum) -> { - final Map c = new LinkedHashMap<>(); - c.put("id", rs.getLong("id")); - c.put("chargeId", rs.getLong("charge_id")); - c.put("name", rs.getString("charge_name")); - - final int chargeTimeEnum = rs.getInt("charge_time_enum"); - c.put("chargeTimeType", buildEnumOption(chargeTimeEnum, chargeTimeValue(chargeTimeEnum))); - - c.put("chargeDueDate", rs.getDate("charge_due_date") != null ? rs.getDate("charge_due_date").toLocalDate().toString() : null); - c.put("feeOnMonthDay", buildFeeOnMonthDay(rs.getObject("fee_on_month"), rs.getObject("fee_on_day"))); - c.put("feeInterval", rs.getObject("fee_interval")); - - final int chargeCalcEnum = rs.getInt("charge_calculation_enum"); - c.put("chargeCalculationType", buildEnumOption(chargeCalcEnum, chargeCalcValue(chargeCalcEnum))); - - c.put("amount", rs.getBigDecimal("amount")); - c.put("amountPaid", rs.getBigDecimal("amount_paid_derived")); - c.put("amountWaived", rs.getBigDecimal("amount_waived_derived")); - c.put("amountWrittenOff", rs.getBigDecimal("amount_writtenoff_derived")); - c.put("amountOutstanding", rs.getBigDecimal("amount_outstanding_derived")); - c.put("paid", rs.getBoolean("is_paid_derived")); - c.put("waived", rs.getBoolean("waived")); - c.put("penalty", rs.getBoolean("is_penalty")); - c.put("isActive", rs.getBoolean("is_active")); - return c; - }, accountId); - } - - private Map buildFeeOnMonthDay(Object month, Object day) { - if (month == null || day == null) return null; - final Map m = new LinkedHashMap<>(); - m.put("month", month); - m.put("day", day); - return m; - } - - private String chargeTimeValue(int id) { - return switch (id) { - case 1 -> "Disbursement"; - case 2 -> "Specified due date"; - case 3 -> "Savings Activation"; - case 4 -> "Withdrawal Fee"; - case 5 -> "Annual Fee"; - case 6 -> "Annual Fee"; - case 7 -> "Monthly Fee"; - case 8 -> "Overdue Fee"; - default -> "Specified due date"; - }; - } - - private String chargeCalcValue(int id) { - return switch (id) { - case 1 -> "Flat"; - case 2 -> "% Amount"; - case 3 -> "% Loan Amount + Interest"; - case 4 -> "% Interest"; - default -> "Flat"; - }; - } - - @Override - public List> retrieveAll() { - List> rows = jdbcTemplate.query(BASE_SQL, new FlatRowMapper()); - List> result = new ArrayList<>(); - for (Map row : rows) { - final Map response = buildStructuredResponse(row); - final Long id = ((Number) row.get("id")).longValue(); - response.put("charges", retrieveCharges(id)); - result.add(response); - } - return result; - } - - private Map buildStructuredResponse(Map row) { - Map out = new LinkedHashMap<>(); - - out.put("id", row.get("id")); - out.put("accountNo", row.get("account_no")); - out.put("externalId", row.get("external_id")); - out.put("clientId", row.get("client_id")); - out.put("clientName", row.get("client_name")); - out.put("clientAccountNo", row.get("client_account_no")); - out.put("groupId", row.get("group_id")); - out.put("productId", row.get("product_id")); - out.put("productName", row.get("product_name")); - out.put("currentAccountProductName", row.get("product_name")); - out.put("shortProductName", row.get("product_short_name")); - out.put("officeId", row.get("office_id")); - out.put("officeName", row.get("office_name")); - out.put("staffId", row.get("staff_id")); - out.put("fieldOfficerId", row.get("staff_id")); - out.put("fieldOfficerName", row.get("staff_name")); - - // status - int statusEnum = toInt(row.get("status_enum"), 100); - out.put("status", buildStatus(statusEnum)); - - // subStatus - int subStatusEnum = toInt(row.get("sub_status_enum"), 0); - out.put("subStatus", buildSubStatus(subStatusEnum)); - - // accountType - int accountTypeEnum = toInt(row.get("account_type_enum"), 1); - out.put("accountType", buildAccountType(accountTypeEnum)); - - // timeline - out.put("timeline", buildTimeline(row)); - - // currency - out.put("currency", buildCurrency(row)); - - // interest settings - out.put("nominalAnnualInterestRate", row.get("nominal_annual_interest_rate")); - - int compPeriod = toInt(row.get("interest_compounding_period_enum"), 4); - out.put("interestCompoundingPeriodType", buildEnumOption(compPeriod, interestCompoundingPeriodValue(compPeriod))); - - int postingPeriod = toInt(row.get("interest_posting_period_enum"), 4); - out.put("interestPostingPeriodType", buildEnumOption(postingPeriod, interestPostingPeriodValue(postingPeriod))); - - int calcType = toInt(row.get("interest_calculation_type_enum"), 1); - out.put("interestCalculationType", buildEnumOption(calcType, interestCalcTypeValue(calcType))); - - int daysInYear = toInt(row.get("interest_calculation_days_in_year_type_enum"), 365); - out.put("interestCalculationDaysInYearType", buildEnumOption(daysInYear, daysInYearValue(daysInYear))); - - out.put("minBalanceForInterestCalculation", row.get("min_balance_for_interest_calculation")); - - // balance/overdraft - out.put("allowOverdraft", row.get("allow_overdraft")); - out.put("overdraftLimit", row.get("overdraft_limit")); - out.put("minOverdraftForInterestCalculation", row.get("min_overdraft_for_interest_calculation")); - out.put("minRequiredBalance", row.get("min_required_balance")); - out.put("enforceMinRequiredBalance", row.get("enforce_min_required_balance")); - out.put("onHoldFunds", row.get("on_hold_funds_derived")); - out.put("withHoldTax", row.get("withhold_tax")); - out.put("taxGroup", null); - - // summary - out.put("summary", buildSummary(row)); - - // CBS-specific fields - out.put("cbsAccountType", row.get("cbs_account_type")); - out.put("customerSegment", row.get("customer_segment")); - out.put("accountStatus", row.get("account_status")); - out.put("modeOfOperation", row.get("mode_of_operation")); - out.put("specialCategory", row.get("special_category")); - out.put("msme", row.get("msme")); - out.put("autoDebitFacility", row.get("auto_debit_facility")); - out.put("balanceSwapOption", row.get("balance_swap_option")); - out.put("establishmentDate", row.get("establishment_date")); - out.put("startOperationDate", row.get("start_operation_date")); - out.put("nonIndividualClient", row.get("is_non_individual_client")); - out.put("signatoryRights", row.get("signatory_rights")); - out.put("introducerClientId", row.get("introducer_client_id")); - out.put("introducerName", row.get("introducer_name")); - out.put("introducerCifNumber", row.get("introducer_cif_number")); - out.put("nomineeData", row.get("nominee_data")); - - return out; - } - - private Map buildStatus(int statusEnum) { - Map s = new LinkedHashMap<>(); - s.put("id", statusEnum); - switch (statusEnum) { - case 100 -> { - s.put("code", "savingsAccountStatusType.submittedAndPendingApproval"); - s.put("value", "Submitted and Pending Approval"); - s.put("submittedAndPendingApproval", true); - s.put("approved", false); s.put("active", false); - s.put("rejected", false); s.put("withdrawnByApplicant", false); s.put("closed", false); - } - case 200 -> { - s.put("code", "savingsAccountStatusType.approved"); - s.put("value", "Approved"); - s.put("submittedAndPendingApproval", false); - s.put("approved", true); s.put("active", false); - s.put("rejected", false); s.put("withdrawnByApplicant", false); s.put("closed", false); - } - case 300 -> { - s.put("code", "savingsAccountStatusType.active"); - s.put("value", "Active"); - s.put("submittedAndPendingApproval", false); - s.put("approved", false); s.put("active", true); - s.put("rejected", false); s.put("withdrawnByApplicant", false); s.put("closed", false); - } - case 400 -> { - s.put("code", "savingsAccountStatusType.withdrawn"); - s.put("value", "Withdrawn by Applicant"); - s.put("submittedAndPendingApproval", false); - s.put("approved", false); s.put("active", false); - s.put("rejected", false); s.put("withdrawnByApplicant", true); s.put("closed", false); - } - case 500 -> { - s.put("code", "savingsAccountStatusType.rejected"); - s.put("value", "Rejected"); - s.put("submittedAndPendingApproval", false); - s.put("approved", false); s.put("active", false); - s.put("rejected", true); s.put("withdrawnByApplicant", false); s.put("closed", false); - } - case 600 -> { - s.put("code", "savingsAccountStatusType.closed"); - s.put("value", "Closed"); - s.put("submittedAndPendingApproval", false); - s.put("approved", false); s.put("active", false); - s.put("rejected", false); s.put("withdrawnByApplicant", false); s.put("closed", true); - } - default -> { - s.put("code", "savingsAccountStatusType.invalid"); - s.put("value", "Invalid"); - s.put("submittedAndPendingApproval", false); - s.put("approved", false); s.put("active", false); - s.put("rejected", false); s.put("withdrawnByApplicant", false); s.put("closed", false); - } - } - return s; - } - - private Map buildSubStatus(int subStatusEnum) { - Map s = new LinkedHashMap<>(); - s.put("id", subStatusEnum); - String value = switch (subStatusEnum) { - case 0 -> "None"; - case 100 -> "Inactive"; - case 200 -> "Dormant"; - case 300 -> "Block"; - case 400 -> "Block Debit"; - case 500 -> "Block Credit"; - default -> "None"; - }; - s.put("code", "SavingsAccountSubStatusEnum." + value.replace(" ", "")); - s.put("value", value); - s.put("none", subStatusEnum == 0); - s.put("inactive", subStatusEnum == 100); - s.put("dormant", subStatusEnum == 200); - s.put("block", subStatusEnum == 300); - s.put("blockDebit", subStatusEnum == 400); - s.put("blockCredit", subStatusEnum == 500); - return s; - } - - private Map buildAccountType(int accountTypeEnum) { - Map s = new LinkedHashMap<>(); - s.put("id", accountTypeEnum); - String value = switch (accountTypeEnum) { - case 1 -> "Individual"; - case 2 -> "Group"; - case 3 -> "JLG"; - default -> "Individual"; - }; - s.put("code", "loanType." + value.toLowerCase()); - s.put("value", value); - return s; - } - - private Map buildTimeline(Map row) { - Map t = new LinkedHashMap<>(); - t.put("submittedOnDate", row.get("submittedon_date")); - t.put("approvedOnDate", row.get("approvedon_date")); - t.put("activatedOnDate", row.get("activatedon_date")); - t.put("rejectedOnDate", row.get("rejectedon_date")); - t.put("withdrawnOnDate", row.get("withdrawnon_date")); - t.put("closedOnDate", row.get("closedon_date")); - return t; - } - - private Map buildCurrency(Map row) { - Map c = new LinkedHashMap<>(); - c.put("code", row.get("currency_code")); - c.put("name", row.get("currency_name")); - c.put("decimalPlaces", row.get("currency_digits")); - c.put("inMultiplesOf", row.get("currency_multiplesof")); - c.put("displaySymbol", row.get("currency_display_symbol")); - c.put("nameCode", row.get("currency_name_code")); - c.put("displayLabel", row.get("currency_name") + " [" + row.get("currency_code") + "]"); - return c; - } - - private Map buildSummary(Map row) { - Map s = new LinkedHashMap<>(); - s.put("accountBalance", row.get("account_balance_derived")); - s.put("availableBalance", row.get("account_balance_derived")); - s.put("totalDeposits", row.get("total_deposits_derived")); - s.put("totalWithdrawals", row.get("total_withdrawals_derived")); - s.put("totalInterestEarned", row.get("total_interest_earned_derived")); - s.put("totalInterestPosted", row.get("total_interest_posted_derived")); - s.put("interestNotPosted", computeInterestNotPosted(row)); - s.put("totalOverdraftInterestDerived", row.get("total_overdraft_interest_derived")); - s.put("totalWithholdTax", row.get("total_withhold_tax_derived")); - s.put("totalWithdrawalFees", null); - s.put("totalAnnualFees", null); - s.put("lastInterestCalculationDate", row.get("last_interest_calculation_date")); - s.put("onHoldFunds", row.get("on_hold_funds_derived")); - return s; - } - - private BigDecimal computeInterestNotPosted(Map row) { - BigDecimal earned = toBigDecimal(row.get("total_interest_earned_derived")); - BigDecimal posted = toBigDecimal(row.get("total_interest_posted_derived")); - if (earned == null) return BigDecimal.ZERO; - if (posted == null) return earned; - return earned.subtract(posted); - } - - private Map buildEnumOption(int id, String value) { - Map e = new LinkedHashMap<>(); - e.put("id", id); - e.put("code", value.replace(" ", "")); - e.put("value", value); - return e; - } - - private String interestCompoundingPeriodValue(int id) { - return switch (id) { - case 1 -> "Daily"; - case 2 -> "Weekly"; - case 3 -> "Bi-Weekly"; - case 4 -> "Monthly"; - case 5 -> "Quarterly"; - case 6 -> "Semi-Annual"; - case 7 -> "Annually"; - default -> "Monthly"; - }; - } - - private String interestPostingPeriodValue(int id) { - return switch (id) { - case 1 -> "Monthly"; - case 2 -> "Quarterly"; - case 3 -> "Biannual"; - case 4 -> "Annually"; - default -> "Annually"; - }; - } - - private String interestCalcTypeValue(int id) { - return switch (id) { - case 1 -> "Daily Balance"; - case 2 -> "Average Daily Balance"; - default -> "Daily Balance"; - }; - } - - private String daysInYearValue(int id) { - return switch (id) { - case 360 -> "360 Days"; - case 365 -> "365 Days"; - case 366 -> "Actual Days"; - default -> "365 Days"; - }; - } - - private int toInt(Object val, int defaultVal) { - if (val == null) return defaultVal; - if (val instanceof Number) return ((Number) val).intValue(); - try { return Integer.parseInt(val.toString().trim()); } catch (Exception e) { return defaultVal; } - } - - private BigDecimal toBigDecimal(Object val) { - if (val == null) return null; - if (val instanceof BigDecimal) return (BigDecimal) val; - try { return new BigDecimal(val.toString().trim()); } catch (Exception e) { return null; } - } - - private static class FlatRowMapper implements RowMapper> { - @Override - public Map mapRow(ResultSet rs, int rowNum) throws SQLException { - ResultSetMetaData meta = rs.getMetaData(); - int cols = meta.getColumnCount(); - Map row = new LinkedHashMap<>(); - for (int i = 1; i <= cols; i++) { - row.put(meta.getColumnLabel(i), rs.getObject(i)); - } - return row; - } - } -} +/** + * 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.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.portfolio.currentaccount.exception.CurrentAccountNotFoundException; +import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountReadPlatformService; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class CurrentAccountReadPlatformServiceImpl implements CurrentAccountReadPlatformService { + + private static final String BASE_SQL = + "SELECT ca.*, " + + "c.display_name AS client_name, c.account_no AS client_account_no, " + + "o.name AS office_name, " + + "s.display_name AS staff_name, " + + "cp.name AS product_name, cp.short_name AS product_short_name, " + + "curr.name AS currency_name, curr.display_symbol AS currency_display_symbol, " + + "curr.internationalized_name_code AS currency_name_code " + + "FROM m_current_account ca " + + "LEFT JOIN m_client c ON c.id = ca.client_id " + + "LEFT JOIN m_office o ON o.id = ca.office_id " + + "LEFT JOIN m_staff s ON s.id = ca.staff_id " + + "LEFT JOIN m_current_product cp ON cp.id = ca.product_id " + + "LEFT JOIN m_currency curr ON curr.code = ca.currency_code "; + + private final JdbcTemplate jdbcTemplate; + + @Override + public Map retrieveOne(Long accountId) { + List> rows = jdbcTemplate.query( + BASE_SQL + "WHERE ca.id = ?", + new FlatRowMapper(), + accountId); + if (rows.isEmpty()) { + throw new CurrentAccountNotFoundException(accountId); + } + final Map response = buildStructuredResponse(rows.get(0)); + response.put("charges", retrieveCharges(accountId)); + response.put("transactions", retrieveTransactions(accountId)); + return response; + } + + private List> retrieveTransactions(Long accountId) { + final String sql = + "SELECT t.id, t.transaction_type_enum, t.transaction_date, t.amount, " + + "t.running_balance_derived, t.is_reversed, t.is_reversal, t.ref_no, t.remarks, " + + "t.submitted_on_date " + + "FROM m_current_account_transaction t " + + "WHERE t.current_account_id = ? " + + "ORDER BY t.transaction_date ASC, t.id ASC"; + + return jdbcTemplate.query(sql, (rs, rowNum) -> { + final Map t = new LinkedHashMap<>(); + t.put("id", rs.getLong("id")); + + final int txnTypeEnum = rs.getInt("transaction_type_enum"); + t.put("transactionType", buildTransactionType(txnTypeEnum)); + + final java.time.LocalDate txnDate = rs.getObject("transaction_date", java.time.LocalDate.class); + if (txnDate != null) { + t.put("date", new int[]{txnDate.getYear(), txnDate.getMonthValue(), txnDate.getDayOfMonth()}); + } else { + t.put("date", null); + } + + t.put("amount", rs.getBigDecimal("amount")); + t.put("runningBalance", rs.getBigDecimal("running_balance_derived")); + t.put("reversed", rs.getBoolean("is_reversed")); + t.put("isReversal", rs.getBoolean("is_reversal")); + t.put("refNo", rs.getString("ref_no")); + t.put("remarks", rs.getString("remarks")); + + final java.time.LocalDate submittedOn = rs.getObject("submitted_on_date", java.time.LocalDate.class); + if (submittedOn != null) { + t.put("submittedOnDate", new int[]{submittedOn.getYear(), submittedOn.getMonthValue(), submittedOn.getDayOfMonth()}); + } else { + t.put("submittedOnDate", null); + } + return t; + }, accountId); + } + + private Map buildTransactionType(int typeEnum) { + final Map tt = new LinkedHashMap<>(); + tt.put("id", typeEnum); + tt.put("deposit", typeEnum == 1); + tt.put("withdrawal", typeEnum == 2); + tt.put("interestPosting", typeEnum == 3); + tt.put("feeDeduction", typeEnum == 4 || typeEnum == 7); + tt.put("overdraftInterest", false); + tt.put("withholdTax", false); + tt.put("initiateTransfer", false); + tt.put("approveTransfer", false); + tt.put("withdrawTransfer", false); + tt.put("rejectTransfer", false); + tt.put("writtenoff", false); + tt.put("overdraftFee", false); + tt.put("escheat", false); + tt.put("amountHold", false); + tt.put("amountRelease", false); + tt.put("accrual", false); + switch (typeEnum) { + case 1 -> { tt.put("code", "savingsTransactionType.deposit"); tt.put("value", "Deposit"); } + case 2 -> { tt.put("code", "savingsTransactionType.withdrawal"); tt.put("value", "Withdrawal"); } + case 3 -> { tt.put("code", "savingsTransactionType.interestPosting"); tt.put("value", "Interest Posting"); } + case 4 -> { tt.put("code", "savingsTransactionType.feeDeduction"); tt.put("value", "Fee Deduction"); } + case 7 -> { tt.put("code", "savingsTransactionType.payCharge"); tt.put("value", "Charge Deduction"); } + default -> { tt.put("code", "savingsTransactionType.deposit"); tt.put("value", "Deposit"); } + } + return tt; + } + + private List> retrieveCharges(Long accountId) { + final String sql = + "SELECT cac.id, cac.charge_id, ch.name AS charge_name, " + + "cac.charge_time_enum, cac.charge_due_date, cac.fee_on_month, cac.fee_on_day, " + + "cac.fee_interval, cac.charge_calculation_enum, " + + "cac.amount, cac.amount_paid_derived, cac.amount_waived_derived, " + + "cac.amount_writtenoff_derived, cac.amount_outstanding_derived, " + + "cac.is_paid_derived, cac.waived, cac.is_penalty, cac.is_active " + + "FROM m_current_account_charge cac " + + "JOIN m_charge ch ON ch.id = cac.charge_id " + + "WHERE cac.current_account_id = ? AND cac.is_active = true " + + "ORDER BY cac.id"; + + return jdbcTemplate.query(sql, (rs, rowNum) -> { + final Map c = new LinkedHashMap<>(); + c.put("id", rs.getLong("id")); + c.put("chargeId", rs.getLong("charge_id")); + c.put("name", rs.getString("charge_name")); + + final int chargeTimeEnum = rs.getInt("charge_time_enum"); + c.put("chargeTimeType", buildEnumOption(chargeTimeEnum, chargeTimeValue(chargeTimeEnum))); + + c.put("chargeDueDate", rs.getDate("charge_due_date") != null ? rs.getDate("charge_due_date").toLocalDate().toString() : null); + c.put("feeOnMonthDay", buildFeeOnMonthDay(rs.getObject("fee_on_month"), rs.getObject("fee_on_day"))); + c.put("feeInterval", rs.getObject("fee_interval")); + + final int chargeCalcEnum = rs.getInt("charge_calculation_enum"); + c.put("chargeCalculationType", buildEnumOption(chargeCalcEnum, chargeCalcValue(chargeCalcEnum))); + + c.put("amount", rs.getBigDecimal("amount")); + c.put("amountPaid", rs.getBigDecimal("amount_paid_derived")); + c.put("amountWaived", rs.getBigDecimal("amount_waived_derived")); + c.put("amountWrittenOff", rs.getBigDecimal("amount_writtenoff_derived")); + c.put("amountOutstanding", rs.getBigDecimal("amount_outstanding_derived")); + c.put("paid", rs.getBoolean("is_paid_derived")); + c.put("waived", rs.getBoolean("waived")); + c.put("penalty", rs.getBoolean("is_penalty")); + c.put("isActive", rs.getBoolean("is_active")); + return c; + }, accountId); + } + + private Map buildFeeOnMonthDay(Object month, Object day) { + if (month == null || day == null) return null; + final Map m = new LinkedHashMap<>(); + m.put("month", month); + m.put("day", day); + return m; + } + + private String chargeTimeValue(int id) { + return switch (id) { + case 1 -> "Disbursement"; + case 2 -> "Specified due date"; + case 3 -> "Savings Activation"; + case 4 -> "Withdrawal Fee"; + case 5 -> "Annual Fee"; + case 6 -> "Annual Fee"; + case 7 -> "Monthly Fee"; + case 8 -> "Overdue Fee"; + default -> "Specified due date"; + }; + } + + private String chargeCalcValue(int id) { + return switch (id) { + case 1 -> "Flat"; + case 2 -> "% Amount"; + case 3 -> "% Loan Amount + Interest"; + case 4 -> "% Interest"; + default -> "Flat"; + }; + } + + @Override + public List> retrieveAll() { + List> rows = jdbcTemplate.query(BASE_SQL, new FlatRowMapper()); + List> result = new ArrayList<>(); + for (Map row : rows) { + final Map response = buildStructuredResponse(row); + final Long id = ((Number) row.get("id")).longValue(); + response.put("charges", retrieveCharges(id)); + result.add(response); + } + return result; + } + + @Override + public List> retrieveAll(final Integer offset, final Integer limit, final String accountNo, + final String clientName) { + final StringBuilder sqlBuilder = new StringBuilder(BASE_SQL); + final List params = new ArrayList<>(); + boolean hasWhere = false; + + if (StringUtils.isNotBlank(accountNo)) { + sqlBuilder.append(hasWhere ? " AND " : " WHERE "); + sqlBuilder.append("ca.account_no LIKE ?"); + params.add("%" + accountNo + "%"); + hasWhere = true; + } + if (StringUtils.isNotBlank(clientName)) { + sqlBuilder.append(hasWhere ? " AND " : " WHERE "); + sqlBuilder.append("c.display_name LIKE ?"); + params.add("%" + clientName + "%"); + hasWhere = true; + } + if (limit != null && limit > 0) { + sqlBuilder.append(" LIMIT ?"); + params.add(limit); + if (offset != null && offset >= 0) { + sqlBuilder.append(" OFFSET ?"); + params.add(offset); + } + } + + List> rows = jdbcTemplate.query(sqlBuilder.toString(), new FlatRowMapper(), params.toArray()); + List> result = new ArrayList<>(); + for (Map row : rows) { + final Map response = buildStructuredResponse(row); + final Long id = ((Number) row.get("id")).longValue(); + response.put("charges", retrieveCharges(id)); + result.add(response); + } + return result; + } + + private Map buildStructuredResponse(Map row) { + Map out = new LinkedHashMap<>(); + + out.put("id", row.get("id")); + out.put("accountNo", row.get("account_no")); + out.put("externalId", row.get("external_id")); + out.put("clientId", row.get("client_id")); + out.put("clientName", row.get("client_name")); + out.put("clientAccountNo", row.get("client_account_no")); + out.put("groupId", row.get("group_id")); + out.put("productId", row.get("product_id")); + out.put("productName", row.get("product_name")); + out.put("currentAccountProductName", row.get("product_name")); + out.put("shortProductName", row.get("product_short_name")); + out.put("officeId", row.get("office_id")); + out.put("officeName", row.get("office_name")); + out.put("staffId", row.get("staff_id")); + out.put("fieldOfficerId", row.get("staff_id")); + out.put("fieldOfficerName", row.get("staff_name")); + + // status + int statusEnum = toInt(row.get("status_enum"), 100); + out.put("status", buildStatus(statusEnum)); + + // subStatus + int subStatusEnum = toInt(row.get("sub_status_enum"), 0); + out.put("subStatus", buildSubStatus(subStatusEnum)); + + // accountType + int accountTypeEnum = toInt(row.get("account_type_enum"), 1); + out.put("accountType", buildAccountType(accountTypeEnum)); + + // timeline + out.put("timeline", buildTimeline(row)); + + // currency + out.put("currency", buildCurrency(row)); + + // interest settings + out.put("nominalAnnualInterestRate", row.get("nominal_annual_interest_rate")); + + int compPeriod = toInt(row.get("interest_compounding_period_enum"), 4); + out.put("interestCompoundingPeriodType", buildEnumOption(compPeriod, interestCompoundingPeriodValue(compPeriod))); + + int postingPeriod = toInt(row.get("interest_posting_period_enum"), 4); + out.put("interestPostingPeriodType", buildEnumOption(postingPeriod, interestPostingPeriodValue(postingPeriod))); + + int calcType = toInt(row.get("interest_calculation_type_enum"), 1); + out.put("interestCalculationType", buildEnumOption(calcType, interestCalcTypeValue(calcType))); + + int daysInYear = toInt(row.get("interest_calculation_days_in_year_type_enum"), 365); + out.put("interestCalculationDaysInYearType", buildEnumOption(daysInYear, daysInYearValue(daysInYear))); + + out.put("minBalanceForInterestCalculation", row.get("min_balance_for_interest_calculation")); + + // balance/overdraft + out.put("allowOverdraft", row.get("allow_overdraft")); + out.put("overdraftLimit", row.get("overdraft_limit")); + out.put("minOverdraftForInterestCalculation", row.get("min_overdraft_for_interest_calculation")); + out.put("minRequiredBalance", row.get("min_required_balance")); + out.put("enforceMinRequiredBalance", row.get("enforce_min_required_balance")); + out.put("onHoldFunds", row.get("on_hold_funds_derived")); + out.put("withHoldTax", row.get("withhold_tax")); + out.put("taxGroup", null); + + // summary + out.put("summary", buildSummary(row)); + + // CBS-specific fields + out.put("cbsAccountType", row.get("cbs_account_type")); + out.put("customerSegment", row.get("customer_segment")); + out.put("accountStatus", row.get("account_status")); + out.put("modeOfOperation", row.get("mode_of_operation")); + out.put("specialCategory", row.get("special_category")); + out.put("msme", row.get("msme")); + out.put("autoDebitFacility", row.get("auto_debit_facility")); + out.put("balanceSwapOption", row.get("balance_swap_option")); + out.put("establishmentDate", row.get("establishment_date")); + out.put("startOperationDate", row.get("start_operation_date")); + out.put("nonIndividualClient", row.get("is_non_individual_client")); + out.put("signatoryRights", row.get("signatory_rights")); + out.put("introducerClientId", row.get("introducer_client_id")); + out.put("introducerName", row.get("introducer_name")); + out.put("introducerCifNumber", row.get("introducer_cif_number")); + out.put("nomineeData", row.get("nominee_data")); + + return out; + } + + private Map buildStatus(int statusEnum) { + Map s = new LinkedHashMap<>(); + s.put("id", statusEnum); + switch (statusEnum) { + case 100 -> { + s.put("code", "savingsAccountStatusType.submittedAndPendingApproval"); + s.put("value", "Submitted and Pending Approval"); + s.put("submittedAndPendingApproval", true); + s.put("approved", false); s.put("active", false); + s.put("rejected", false); s.put("withdrawnByApplicant", false); s.put("closed", false); + } + case 200 -> { + s.put("code", "savingsAccountStatusType.approved"); + s.put("value", "Approved"); + s.put("submittedAndPendingApproval", false); + s.put("approved", true); s.put("active", false); + s.put("rejected", false); s.put("withdrawnByApplicant", false); s.put("closed", false); + } + case 300 -> { + s.put("code", "savingsAccountStatusType.active"); + s.put("value", "Active"); + s.put("submittedAndPendingApproval", false); + s.put("approved", false); s.put("active", true); + s.put("rejected", false); s.put("withdrawnByApplicant", false); s.put("closed", false); + } + case 400 -> { + s.put("code", "savingsAccountStatusType.withdrawn"); + s.put("value", "Withdrawn by Applicant"); + s.put("submittedAndPendingApproval", false); + s.put("approved", false); s.put("active", false); + s.put("rejected", false); s.put("withdrawnByApplicant", true); s.put("closed", false); + } + case 500 -> { + s.put("code", "savingsAccountStatusType.rejected"); + s.put("value", "Rejected"); + s.put("submittedAndPendingApproval", false); + s.put("approved", false); s.put("active", false); + s.put("rejected", true); s.put("withdrawnByApplicant", false); s.put("closed", false); + } + case 600 -> { + s.put("code", "savingsAccountStatusType.closed"); + s.put("value", "Closed"); + s.put("submittedAndPendingApproval", false); + s.put("approved", false); s.put("active", false); + s.put("rejected", false); s.put("withdrawnByApplicant", false); s.put("closed", true); + } + default -> { + s.put("code", "savingsAccountStatusType.invalid"); + s.put("value", "Invalid"); + s.put("submittedAndPendingApproval", false); + s.put("approved", false); s.put("active", false); + s.put("rejected", false); s.put("withdrawnByApplicant", false); s.put("closed", false); + } + } + return s; + } + + private Map buildSubStatus(int subStatusEnum) { + Map s = new LinkedHashMap<>(); + s.put("id", subStatusEnum); + String value = switch (subStatusEnum) { + case 0 -> "None"; + case 100 -> "Inactive"; + case 200 -> "Dormant"; + case 300 -> "Block"; + case 400 -> "Block Debit"; + case 500 -> "Block Credit"; + default -> "None"; + }; + s.put("code", "SavingsAccountSubStatusEnum." + value.replace(" ", "")); + s.put("value", value); + s.put("none", subStatusEnum == 0); + s.put("inactive", subStatusEnum == 100); + s.put("dormant", subStatusEnum == 200); + s.put("block", subStatusEnum == 300); + s.put("blockDebit", subStatusEnum == 400); + s.put("blockCredit", subStatusEnum == 500); + return s; + } + + private Map buildAccountType(int accountTypeEnum) { + Map s = new LinkedHashMap<>(); + s.put("id", accountTypeEnum); + String value = switch (accountTypeEnum) { + case 1 -> "Individual"; + case 2 -> "Group"; + case 3 -> "JLG"; + default -> "Individual"; + }; + s.put("code", "loanType." + value.toLowerCase()); + s.put("value", value); + return s; + } + + private Map buildTimeline(Map row) { + Map t = new LinkedHashMap<>(); + t.put("submittedOnDate", row.get("submittedon_date")); + t.put("approvedOnDate", row.get("approvedon_date")); + t.put("activatedOnDate", row.get("activatedon_date")); + t.put("rejectedOnDate", row.get("rejectedon_date")); + t.put("withdrawnOnDate", row.get("withdrawnon_date")); + t.put("closedOnDate", row.get("closedon_date")); + return t; + } + + private Map buildCurrency(Map row) { + Map c = new LinkedHashMap<>(); + c.put("code", row.get("currency_code")); + c.put("name", row.get("currency_name")); + c.put("decimalPlaces", row.get("currency_digits")); + c.put("inMultiplesOf", row.get("currency_multiplesof")); + c.put("displaySymbol", row.get("currency_display_symbol")); + c.put("nameCode", row.get("currency_name_code")); + c.put("displayLabel", row.get("currency_name") + " [" + row.get("currency_code") + "]"); + return c; + } + + private Map buildSummary(Map row) { + Map s = new LinkedHashMap<>(); + s.put("accountBalance", row.get("account_balance_derived")); + s.put("availableBalance", row.get("account_balance_derived")); + s.put("totalDeposits", row.get("total_deposits_derived")); + s.put("totalWithdrawals", row.get("total_withdrawals_derived")); + s.put("totalInterestEarned", row.get("total_interest_earned_derived")); + s.put("totalInterestPosted", row.get("total_interest_posted_derived")); + s.put("interestNotPosted", computeInterestNotPosted(row)); + s.put("totalOverdraftInterestDerived", row.get("total_overdraft_interest_derived")); + s.put("totalWithholdTax", row.get("total_withhold_tax_derived")); + s.put("totalWithdrawalFees", null); + s.put("totalAnnualFees", null); + s.put("lastInterestCalculationDate", row.get("last_interest_calculation_date")); + s.put("onHoldFunds", row.get("on_hold_funds_derived")); + return s; + } + + private BigDecimal computeInterestNotPosted(Map row) { + BigDecimal earned = toBigDecimal(row.get("total_interest_earned_derived")); + BigDecimal posted = toBigDecimal(row.get("total_interest_posted_derived")); + if (earned == null) return BigDecimal.ZERO; + if (posted == null) return earned; + return earned.subtract(posted); + } + + private Map buildEnumOption(int id, String value) { + Map e = new LinkedHashMap<>(); + e.put("id", id); + e.put("code", value.replace(" ", "")); + e.put("value", value); + return e; + } + + private String interestCompoundingPeriodValue(int id) { + return switch (id) { + case 1 -> "Daily"; + case 2 -> "Weekly"; + case 3 -> "Bi-Weekly"; + case 4 -> "Monthly"; + case 5 -> "Quarterly"; + case 6 -> "Semi-Annual"; + case 7 -> "Annually"; + default -> "Monthly"; + }; + } + + private String interestPostingPeriodValue(int id) { + return switch (id) { + case 1 -> "Monthly"; + case 2 -> "Quarterly"; + case 3 -> "Biannual"; + case 4 -> "Annually"; + default -> "Annually"; + }; + } + + private String interestCalcTypeValue(int id) { + return switch (id) { + case 1 -> "Daily Balance"; + case 2 -> "Average Daily Balance"; + default -> "Daily Balance"; + }; + } + + private String daysInYearValue(int id) { + return switch (id) { + case 360 -> "360 Days"; + case 365 -> "365 Days"; + case 366 -> "Actual Days"; + default -> "365 Days"; + }; + } + + private int toInt(Object val, int defaultVal) { + if (val == null) return defaultVal; + if (val instanceof Number) return ((Number) val).intValue(); + try { return Integer.parseInt(val.toString().trim()); } catch (Exception e) { return defaultVal; } + } + + private BigDecimal toBigDecimal(Object val) { + if (val == null) return null; + if (val instanceof BigDecimal) return (BigDecimal) val; + try { return new BigDecimal(val.toString().trim()); } catch (Exception e) { return null; } + } + + private static class FlatRowMapper implements RowMapper> { + @Override + public Map mapRow(ResultSet rs, int rowNum) throws SQLException { + ResultSetMetaData meta = rs.getMetaData(); + int cols = meta.getColumnCount(); + Map row = new LinkedHashMap<>(); + for (int i = 1; i <= cols; i++) { + row.put(meta.getColumnLabel(i), rs.getObject(i)); + } + return row; + } + } +} 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 new file mode 100644 index 000000000..cde77dd4d --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/CurrentAccountTransactionWritePlatformServiceImpl.java @@ -0,0 +1,468 @@ +/** + * 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 lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForSavings; +import org.apache.fineract.accounting.glaccount.domain.GLAccount; +import org.apache.fineract.accounting.journalentry.domain.JournalEntry; +import org.apache.fineract.accounting.journalentry.domain.JournalEntryRepository; +import org.apache.fineract.accounting.journalentry.domain.JournalEntryType; +import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMappingRepository; +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.GeneralPlatformDomainRuleException; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.organisation.office.domain.Office; +import org.apache.fineract.portfolio.PortfolioProductType; +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.currentaccount.exception.CurrentAccountNotFoundException; +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.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class CurrentAccountTransactionWritePlatformServiceImpl implements CurrentAccountTransactionWritePlatformService { + + // Savings entity type used for GL journal entries (EntityType.SAVINGS = 2) + private static final Integer ENTITY_TYPE_SAVINGS = 2; + + private final PlatformSecurityContext context; + private final CurrentAccountRepository currentAccountRepository; + private final CurrentAccountTransactionRepository currentAccountTransactionRepository; + private final CurrentProductRepository currentProductRepository; + private final ProductToGLAccountMappingRepository productToGLAccountMappingRepository; + private final JournalEntryRepository journalEntryRepository; + + @Transactional + @Override + public CommandProcessingResult deposit(final Long accountId, final JsonCommand command) { + context.authenticatedUser(); + + final CurrentAccount account = findActiveAccount(accountId); + final Office office = account.getOffice(); + + LocalDate txnDate = command.localDateValueOfParameterNamed("transactionDate"); + if (txnDate == null) txnDate = LocalDate.now(); + + final BigDecimal amount = command.bigDecimalValueOfParameterNamed("transactionAmount"); + validatePositiveAmount(amount, accountId, "deposit"); + + final String refNo = command.stringValueOfParameterNamed("accountNumber"); + + final CurrentAccountTransaction txn = CurrentAccountTransaction.deposit(account, office, amount, txnDate, refNo); + + final BigDecimal oldBalance = safeBalance(account.getAccountBalanceDerived()); + final BigDecimal newBalance = oldBalance.add(amount); + txn.setRunningBalanceDerived(newBalance); + + currentAccountTransactionRepository.saveAndFlush(txn); + + account.setAccountBalanceDerived(newBalance); + account.setTotalDepositsDerived(safeBalance(account.getTotalDepositsDerived()).add(amount)); + currentAccountRepository.save(account); + + postTransactionGl(account, txn, JournalEntryType.DEBIT, JournalEntryType.CREDIT, "Current Account Deposit"); + + return new CommandProcessingResultBuilder() + .withEntityId(txn.getId()) + .withSavingsId(accountId) + .withClientId(account.getClient() != null ? account.getClient().getId() : null) + .withOfficeId(office != null ? office.getId() : null) + .build(); + } + + @Transactional + @Override + public CommandProcessingResult withdrawal(final Long accountId, final JsonCommand command) { + context.authenticatedUser(); + + final CurrentAccount account = findActiveAccount(accountId); + final Office office = account.getOffice(); + + LocalDate txnDate = command.localDateValueOfParameterNamed("transactionDate"); + if (txnDate == null) txnDate = LocalDate.now(); + + final BigDecimal amount = command.bigDecimalValueOfParameterNamed("transactionAmount"); + validatePositiveAmount(amount, accountId, "withdrawal"); + + final BigDecimal currentBalance = safeBalance(account.getAccountBalanceDerived()); + final BigDecimal onHold = safeBalance(account.getOnHoldFundsDerived()); + final BigDecimal available = currentBalance.subtract(onHold); + + if (!account.isAllowOverdraft() && amount.compareTo(available) > 0) { + throw new GeneralPlatformDomainRuleException("error.msg.current.account.insufficient.balance", + "Insufficient available balance. Available: " + available + ", Requested: " + amount, accountId); + } + + final String refNo = command.stringValueOfParameterNamed("accountNumber"); + + final CurrentAccountTransaction txn = CurrentAccountTransaction.withdrawal(account, office, amount, txnDate, refNo); + + final BigDecimal newBalance = currentBalance.subtract(amount); + txn.setRunningBalanceDerived(newBalance); + + currentAccountTransactionRepository.saveAndFlush(txn); + + account.setAccountBalanceDerived(newBalance); + account.setTotalWithdrawalsDerived(safeBalance(account.getTotalWithdrawalsDerived()).add(amount)); + currentAccountRepository.save(account); + + // Withdrawal GL: DR SAVINGS_CONTROL (liability down), CR SAVINGS_REFERENCE (cash out) + postTransactionGl(account, txn, JournalEntryType.CREDIT, JournalEntryType.DEBIT, "Current Account Withdrawal"); + + return new CommandProcessingResultBuilder() + .withEntityId(txn.getId()) + .withSavingsId(accountId) + .withClientId(account.getClient() != null ? account.getClient().getId() : null) + .withOfficeId(office != null ? office.getId() : null) + .build(); + } + + @Transactional + @Override + public CommandProcessingResult holdAmount(final Long accountId, final JsonCommand command) { + context.authenticatedUser(); + + final CurrentAccount account = findActiveAccount(accountId); + final Office office = account.getOffice(); + + LocalDate txnDate = command.localDateValueOfParameterNamed("transactionDate"); + if (txnDate == null) txnDate = LocalDate.now(); + + final BigDecimal amount = command.bigDecimalValueOfParameterNamed("transactionAmount"); + validatePositiveAmount(amount, accountId, "hold amount"); + + final BigDecimal currentBalance = safeBalance(account.getAccountBalanceDerived()); + final BigDecimal onHold = safeBalance(account.getOnHoldFundsDerived()); + final BigDecimal available = currentBalance.subtract(onHold); + + if (amount.compareTo(available) > 0) { + throw new GeneralPlatformDomainRuleException("error.msg.current.account.insufficient.balance.for.hold", + "Insufficient available balance to place hold. Available: " + available + ", Requested: " + amount, accountId); + } + + final String reason = command.stringValueOfParameterNamed("reasonForBlock"); + + final CurrentAccountTransaction txn = CurrentAccountTransaction.holdAmount(account, office, amount, txnDate, reason); + txn.setRunningBalanceDerived(currentBalance); + + currentAccountTransactionRepository.saveAndFlush(txn); + + account.setOnHoldFundsDerived(onHold.add(amount)); + currentAccountRepository.save(account); + + return new CommandProcessingResultBuilder() + .withEntityId(txn.getId()) + .withSavingsId(accountId) + .withClientId(account.getClient() != null ? account.getClient().getId() : null) + .withOfficeId(office != null ? office.getId() : null) + .build(); + } + + @Transactional + @Override + public CommandProcessingResult releaseAmount(final Long accountId, final Long transactionId) { + context.authenticatedUser(); + + final CurrentAccount account = findActiveAccount(accountId); + final Office office = account.getOffice(); + + final CurrentAccountTransaction holdTxn = currentAccountTransactionRepository.findById(transactionId) + .orElseThrow(() -> new GeneralPlatformDomainRuleException( + "error.msg.current.account.transaction.not.found", + "Hold transaction not found: " + transactionId, transactionId)); + + if (!holdTxn.getCurrentAccount().getId().equals(accountId)) { + throw new GeneralPlatformDomainRuleException("error.msg.current.account.transaction.not.found", + "Transaction does not belong to this account.", transactionId); + } + + if (holdTxn.isReversed()) { + throw new GeneralPlatformDomainRuleException("error.msg.current.account.transaction.already.reversed", + "Hold transaction is already released/reversed.", transactionId); + } + + final BigDecimal holdAmount = holdTxn.getAmount(); + final BigDecimal currentBalance = safeBalance(account.getAccountBalanceDerived()); + + // Mark hold transaction as reversed + holdTxn.setReversed(true); + currentAccountTransactionRepository.save(holdTxn); + + // Create release transaction + final CurrentAccountTransaction releaseTxn = CurrentAccountTransaction.releaseAmount( + account, office, holdAmount, LocalDate.now(), transactionId); + releaseTxn.setRunningBalanceDerived(currentBalance); + currentAccountTransactionRepository.saveAndFlush(releaseTxn); + + // Reduce onHold + final BigDecimal onHold = safeBalance(account.getOnHoldFundsDerived()); + account.setOnHoldFundsDerived(onHold.subtract(holdAmount).max(BigDecimal.ZERO)); + currentAccountRepository.save(account); + + return new CommandProcessingResultBuilder() + .withEntityId(releaseTxn.getId()) + .withSavingsId(accountId) + .build(); + } + + @Transactional + @Override + public CommandProcessingResult undoTransaction(final Long accountId, final Long transactionId, final boolean isReversal) { + context.authenticatedUser(); + + final CurrentAccount account = findActiveAccount(accountId); + + final CurrentAccountTransaction txn = currentAccountTransactionRepository.findById(transactionId) + .orElseThrow(() -> new GeneralPlatformDomainRuleException( + "error.msg.current.account.transaction.not.found", + "Transaction not found: " + transactionId, transactionId)); + + if (!txn.getCurrentAccount().getId().equals(accountId)) { + throw new GeneralPlatformDomainRuleException("error.msg.current.account.transaction.not.found", + "Transaction does not belong to this account.", transactionId); + } + + if (txn.isReversed()) { + throw new GeneralPlatformDomainRuleException("error.msg.current.account.transaction.already.reversed", + "Transaction is already reversed.", transactionId); + } + + reverseTransactionBalances(account, txn); + + txn.setReversed(true); + currentAccountTransactionRepository.save(txn); + currentAccountRepository.save(account); + + return new CommandProcessingResultBuilder() + .withEntityId(transactionId) + .withSavingsId(accountId) + .build(); + } + + @Transactional + @Override + public CommandProcessingResult reverseTransaction(final Long accountId, final Long transactionId, + final boolean isReversal, final JsonCommand command) { + context.authenticatedUser(); + + final CurrentAccount account = findActiveAccount(accountId); + + final CurrentAccountTransaction original = currentAccountTransactionRepository.findById(transactionId) + .orElseThrow(() -> new GeneralPlatformDomainRuleException( + "error.msg.current.account.transaction.not.found", + "Transaction not found: " + transactionId, transactionId)); + + if (!original.getCurrentAccount().getId().equals(accountId)) { + throw new GeneralPlatformDomainRuleException("error.msg.current.account.transaction.not.found", + "Transaction does not belong to this account.", transactionId); + } + + if (original.isReversed()) { + throw new GeneralPlatformDomainRuleException("error.msg.current.account.transaction.already.reversed", + "Transaction is already reversed.", transactionId); + } + + reverseTransactionBalances(account, original); + original.setReversed(true); + currentAccountTransactionRepository.save(original); + + // Create a reversal entry + final CurrentAccountTransaction reversalTxn = CurrentAccountTransaction.reversal( + account, account.getOffice(), original.getAmount(), LocalDate.now(), transactionId); + reversalTxn.setRunningBalanceDerived(safeBalance(account.getAccountBalanceDerived())); + currentAccountTransactionRepository.saveAndFlush(reversalTxn); + + currentAccountRepository.save(account); + + return new CommandProcessingResultBuilder() + .withEntityId(reversalTxn.getId()) + .withSavingsId(accountId) + .build(); + } + + @Transactional + @Override + public CommandProcessingResult adjustTransaction(final Long accountId, final Long transactionId, + final JsonCommand command) { + context.authenticatedUser(); + + final CurrentAccount account = findActiveAccount(accountId); + final Office office = account.getOffice(); + + final CurrentAccountTransaction original = currentAccountTransactionRepository.findById(transactionId) + .orElseThrow(() -> new GeneralPlatformDomainRuleException( + "error.msg.current.account.transaction.not.found", + "Transaction not found: " + transactionId, transactionId)); + + if (!original.getCurrentAccount().getId().equals(accountId)) { + throw new GeneralPlatformDomainRuleException("error.msg.current.account.transaction.not.found", + "Transaction does not belong to this account.", transactionId); + } + + if (original.isReversed()) { + throw new GeneralPlatformDomainRuleException("error.msg.current.account.transaction.already.reversed", + "Transaction is already reversed.", transactionId); + } + + final BigDecimal newAmount = command.bigDecimalValueOfParameterNamed("transactionAmount"); + validatePositiveAmount(newAmount, accountId, "adjust"); + + LocalDate newDate = command.localDateValueOfParameterNamed("transactionDate"); + if (newDate == null) newDate = original.getTransactionDate(); + + // Undo original + reverseTransactionBalances(account, original); + original.setReversed(true); + currentAccountTransactionRepository.save(original); + + // Create adjusted transaction of the same type + final CurrentAccountTransaction adjusted; + if (original.getTransactionTypeEnum() == 1) { + adjusted = CurrentAccountTransaction.deposit(account, office, newAmount, newDate, + command.stringValueOfParameterNamed("accountNumber")); + } else { + adjusted = CurrentAccountTransaction.withdrawal(account, office, newAmount, newDate, + command.stringValueOfParameterNamed("accountNumber")); + } + adjusted.setOriginalTransactionId(transactionId); + adjusted.setRunningBalanceDerived(safeBalance(account.getAccountBalanceDerived())); + currentAccountTransactionRepository.saveAndFlush(adjusted); + + // Apply new transaction to balance + applyTransactionToBalance(account, adjusted); + currentAccountRepository.save(account); + + return new CommandProcessingResultBuilder() + .withEntityId(adjusted.getId()) + .withSavingsId(accountId) + .build(); + } + + // ── Helpers ───────────────────────────────────────────────────────────────── + + private CurrentAccount findActiveAccount(final Long accountId) { + final CurrentAccount account = currentAccountRepository.findById(accountId) + .orElseThrow(() -> new CurrentAccountNotFoundException(accountId)); + if (!Integer.valueOf(300).equals(account.getStatusEnum())) { + throw new GeneralPlatformDomainRuleException("error.msg.current.account.not.active", + "Current account is not in Active state.", accountId); + } + return account; + } + + private void validatePositiveAmount(final BigDecimal amount, final Long accountId, final String operation) { + if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) { + throw new GeneralPlatformDomainRuleException( + "error.msg.current.account." + operation + ".invalid.amount", + "Transaction amount must be greater than zero for " + operation + ".", accountId); + } + } + + private BigDecimal safeBalance(final BigDecimal value) { + return value != null ? value : BigDecimal.ZERO; + } + + private void reverseTransactionBalances(final CurrentAccount account, final CurrentAccountTransaction txn) { + final BigDecimal amount = txn.getAmount(); + final Integer type = txn.getTransactionTypeEnum(); + + if (Integer.valueOf(1).equals(type)) { + // Was a deposit: reverse it + account.setAccountBalanceDerived(safeBalance(account.getAccountBalanceDerived()).subtract(amount)); + account.setTotalDepositsDerived(safeBalance(account.getTotalDepositsDerived()).subtract(amount).max(BigDecimal.ZERO)); + } else if (Integer.valueOf(2).equals(type)) { + // Was a withdrawal: reverse it + account.setAccountBalanceDerived(safeBalance(account.getAccountBalanceDerived()).add(amount)); + account.setTotalWithdrawalsDerived(safeBalance(account.getTotalWithdrawalsDerived()).subtract(amount).max(BigDecimal.ZERO)); + } + } + + private void applyTransactionToBalance(final CurrentAccount account, final CurrentAccountTransaction txn) { + final BigDecimal amount = txn.getAmount(); + final Integer type = txn.getTransactionTypeEnum(); + + if (Integer.valueOf(1).equals(type)) { + account.setAccountBalanceDerived(safeBalance(account.getAccountBalanceDerived()).add(amount)); + account.setTotalDepositsDerived(safeBalance(account.getTotalDepositsDerived()).add(amount)); + } else if (Integer.valueOf(2).equals(type)) { + account.setAccountBalanceDerived(safeBalance(account.getAccountBalanceDerived()).subtract(amount)); + account.setTotalWithdrawalsDerived(safeBalance(account.getTotalWithdrawalsDerived()).add(amount)); + } + } + + // GL: cashEntryType=DEBIT/CREDIT for SAVINGS_REFERENCE; controlEntryType for SAVINGS_CONTROL + private void postTransactionGl(final CurrentAccount account, final CurrentAccountTransaction txn, + final JournalEntryType cashEntryType, final JournalEntryType controlEntryType, final String description) { + try { + final CurrentProduct product = currentProductRepository.findById(account.getProductId()).orElse(null); + if (product == null) return; + final Integer accountingType = product.getAccountingType(); + if (accountingType == null || accountingType == 1) return; // NONE = 1 + + final GLAccount cashAccount = findGlAccount(account.getProductId(), + CashAccountsForSavings.SAVINGS_REFERENCE.getValue()); + final GLAccount controlAccount = findGlAccount(account.getProductId(), + CashAccountsForSavings.SAVINGS_CONTROL.getValue()); + + if (cashAccount == null || controlAccount == null) { + log.warn("[GL] Missing GL account mapping for current account product id={}", account.getProductId()); + return; + } + + final String ref = "CAT" + txn.getId(); + postGlEntry(account, cashAccount, cashEntryType, txn.getAmount(), txn.getTransactionDate(), ref, description); + postGlEntry(account, controlAccount, controlEntryType, txn.getAmount(), txn.getTransactionDate(), ref, description); + } catch (Exception e) { + log.error("[GL] Failed GL posting for CA transaction id={}: {}", txn.getId(), e.getMessage(), e); + } + } + + private GLAccount findGlAccount(final Long productId, final int financialAccountType) { + final var mapping = productToGLAccountMappingRepository + .findByProductIdAndProductTypeAndFinancialAccountTypeAndPaymentTypeId( + productId, PortfolioProductType.SAVING.getValue(), financialAccountType, null); + return mapping != null ? mapping.getGlAccount() : null; + } + + private void postGlEntry(final CurrentAccount account, final GLAccount glAccount, + final JournalEntryType entryType, final BigDecimal amount, + final LocalDate date, final String transactionRef, final String description) { + final JournalEntry entry = JournalEntry.createNew( + account.getOffice(), null, glAccount, + account.getCurrencyCode(), transactionRef, + false, date, entryType, amount, + description, + ENTITY_TYPE_SAVINGS, account.getId(), + null, null, null, null, null); + journalEntryRepository.save(entry); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountReadPlatformService.java index 38ad12394..69355d32f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountReadPlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountReadPlatformService.java @@ -1,56 +1,59 @@ -/** - * 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.util.Collection; -import java.util.Map; -import org.apache.fineract.infrastructure.core.data.PaginationParameters; -import org.apache.fineract.infrastructure.core.service.Page; -import org.apache.fineract.portfolio.account.data.AccountTransferDTO; -import org.apache.fineract.portfolio.savings.DepositAccountType; -import org.apache.fineract.portfolio.savings.data.DepositAccountData; -import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData; - -public interface DepositAccountReadPlatformService { - - Collection retrieveAll(DepositAccountType depositAccountType, PaginationParameters paginationParameters); - - Page retrieveAllPaged(DepositAccountType depositAccountType, PaginationParameters paginationParameters); - - Collection retrieveAllForLookup(DepositAccountType depositAccountType); - - DepositAccountData retrieveOne(DepositAccountType depositAccountType, Long accountId); - - DepositAccountData retrieveOneWithClosureTemplate(DepositAccountType depositAccountType, Long accountId); - - DepositAccountData retrieveOneWithChartSlabs(DepositAccountType depositAccountType, Long productId); - - Collection retrieveAllTransactions(DepositAccountType depositAccountType, Long accountId); - - DepositAccountData retrieveTemplate(DepositAccountType depositAccountType, Long clientId, Long groupId, Long productId, - boolean staffInSelectedOfficeOnly); - - Collection retrieveForMaturityUpdate(); - - SavingsAccountTransactionData retrieveRecurringAccountDepositTransactionTemplate(Long accountId); - - Collection retrieveDataForInterestTransfer(); - - Collection> retriveDataForRDScheduleCreation(); -} +/** + * 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.util.Collection; +import java.util.Map; +import org.apache.fineract.infrastructure.core.data.PaginationParameters; +import org.apache.fineract.infrastructure.core.service.Page; +import org.apache.fineract.portfolio.account.data.AccountTransferDTO; +import org.apache.fineract.portfolio.savings.DepositAccountType; +import org.apache.fineract.portfolio.savings.data.DepositAccountData; +import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData; + +public interface DepositAccountReadPlatformService { + + Collection retrieveAll(DepositAccountType depositAccountType, PaginationParameters paginationParameters); + + Page retrieveAllPaged(DepositAccountType depositAccountType, PaginationParameters paginationParameters); + + Page retrieveAllPaged(DepositAccountType depositAccountType, PaginationParameters paginationParameters, + String accountNo, String clientName); + + Collection retrieveAllForLookup(DepositAccountType depositAccountType); + + DepositAccountData retrieveOne(DepositAccountType depositAccountType, Long accountId); + + DepositAccountData retrieveOneWithClosureTemplate(DepositAccountType depositAccountType, Long accountId); + + DepositAccountData retrieveOneWithChartSlabs(DepositAccountType depositAccountType, Long productId); + + Collection retrieveAllTransactions(DepositAccountType depositAccountType, Long accountId); + + DepositAccountData retrieveTemplate(DepositAccountType depositAccountType, Long clientId, Long groupId, Long productId, + boolean staffInSelectedOfficeOnly); + + Collection retrieveForMaturityUpdate(); + + SavingsAccountTransactionData retrieveRecurringAccountDepositTransactionTemplate(Long accountId); + + Collection retrieveDataForInterestTransfer(); + + Collection> retriveDataForRDScheduleCreation(); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountReadPlatformServiceImpl.java index fbfde7e34..bd466b09e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountReadPlatformServiceImpl.java @@ -1,1939 +1,1973 @@ -/** - * 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.infrastructure.core.domain.AuditableFieldsConstants.CREATED_BY_DB_FIELD; - -import java.math.BigDecimal; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import lombok.RequiredArgsConstructor; -import org.apache.fineract.infrastructure.codes.data.CodeValueData; -import org.apache.fineract.infrastructure.codes.service.CodeValueReadPlatformService; -import org.apache.fineract.infrastructure.core.data.EnumOptionData; -import org.apache.fineract.infrastructure.core.data.PaginationParameters; -import org.apache.fineract.infrastructure.core.data.PaginationParametersDataValidator; -import org.apache.fineract.infrastructure.core.domain.ExternalId; -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.database.DatabaseSpecificSQLGenerator; -import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; -import org.apache.fineract.organisation.monetary.data.CurrencyData; -import org.apache.fineract.organisation.staff.data.StaffData; -import org.apache.fineract.organisation.staff.service.StaffReadPlatformService; -import org.apache.fineract.portfolio.account.PortfolioAccountType; -import org.apache.fineract.portfolio.account.service.AccountNumberGenerator; -import org.apache.fineract.portfolio.account.data.AccountTransferDTO; -import org.apache.fineract.portfolio.account.data.AccountTransferData; -import org.apache.fineract.portfolio.account.domain.AccountTransferType; -import org.apache.fineract.portfolio.calendar.data.CalendarData; -import org.apache.fineract.portfolio.calendar.domain.CalendarEntityType; -import org.apache.fineract.portfolio.calendar.domain.CalendarFrequencyType; -import org.apache.fineract.portfolio.calendar.domain.CalendarType; -import org.apache.fineract.portfolio.calendar.service.CalendarReadPlatformService; -import org.apache.fineract.portfolio.charge.data.ChargeData; -import org.apache.fineract.portfolio.charge.service.ChargeReadPlatformService; -import org.apache.fineract.portfolio.charge.util.ConvertChargeDataToSpecificChargeData; -import org.apache.fineract.portfolio.client.data.ClientData; -import org.apache.fineract.portfolio.client.service.ClientReadPlatformService; -import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; -import org.apache.fineract.portfolio.common.service.CommonEnumerations; -import org.apache.fineract.portfolio.common.service.DropdownReadPlatformService; -import org.apache.fineract.portfolio.group.data.GroupGeneralData; -import org.apache.fineract.portfolio.group.service.GroupReadPlatformService; -import org.apache.fineract.portfolio.interestratechart.data.InterestRateChartData; -import org.apache.fineract.portfolio.interestratechart.service.InterestRateChartReadPlatformService; -import org.apache.fineract.portfolio.paymentdetail.data.PaymentDetailData; -import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData; -import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeReadService; -import org.apache.fineract.portfolio.savings.SavingsApiConstants; -import org.apache.fineract.portfolio.savings.DepositAccountOnClosureType; -import org.apache.fineract.portfolio.savings.DepositAccountType; -import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType; -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.DepositAccountData; -import org.apache.fineract.portfolio.savings.data.DepositAccountInterestRateChartData; -import org.apache.fineract.portfolio.savings.data.DepositProductData; -import org.apache.fineract.portfolio.savings.data.FixedDepositAccountData; -import org.apache.fineract.portfolio.savings.data.RecurringDepositAccountData; -import org.apache.fineract.portfolio.savings.data.SavingsAccountApplicationTimelineData; -import org.apache.fineract.portfolio.savings.data.SavingsAccountChargeData; -import org.apache.fineract.portfolio.savings.data.SavingsAccountData; -import org.apache.fineract.portfolio.savings.data.SavingsAccountStatusEnumData; -import org.apache.fineract.portfolio.savings.data.SavingsAccountSummaryData; -import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData; -import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionEnumData; -import org.apache.fineract.portfolio.savings.domain.SavingsAccountStatusType; -import org.apache.fineract.portfolio.savings.exception.DepositAccountNotFoundException; -import org.apache.fineract.portfolio.tax.data.TaxGroupData; -import org.apache.fineract.useradministration.domain.AppUser; -import org.springframework.dao.EmptyResultDataAccessException; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.RowMapper; -import lombok.extern.slf4j.Slf4j; -import org.springframework.util.CollectionUtils; - -@Slf4j -@RequiredArgsConstructor -public class DepositAccountReadPlatformServiceImpl implements DepositAccountReadPlatformService { - - private static final FixedDepositAccountMapper FIXED_DEPOSIT_ACCOUNT_MAPPER = new FixedDepositAccountMapper(); - private static final RecurringDepositAccountMapper RECURRING_DEPOSIT_ACCOUNT_MAPPER = new RecurringDepositAccountMapper(); - private static final DepositAccountLookupMapper DEPOSIT_ACCOUNT_LOOKUP_MAPPER = new DepositAccountLookupMapper(); - private static final DepositAccountForMaturityMapper DEPOSIT_ACCOUNT_FOR_MATURITY_MAPPER = new DepositAccountForMaturityMapper(); - // allowed column names for sorting the query result - private static final Set supportedOrderByValues = new HashSet<>(Arrays.asList("id", "accountNumbr", "officeId", "officeName")); - private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); - private final PlatformSecurityContext context; - private final JdbcTemplate jdbcTemplate; - private final DepositAccountInterestRateChartReadPlatformService accountChartReadPlatformService; - private final InterestRateChartReadPlatformService productChartReadPlatformService; - private final PaginationParametersDataValidator paginationParametersDataValidator; - private final DatabaseSpecificSQLGenerator sqlGenerator; - private final PaginationHelper paginationHelper; - private final SavingsAccountTransactionsMapper transactionsMapper = new SavingsAccountTransactionsMapper(); - private final ClientReadPlatformService clientReadPlatformService; - private final GroupReadPlatformService groupReadPlatformService; - private final DepositProductReadPlatformService depositProductReadPlatformService; - private final SavingsDropdownReadPlatformService savingsDropdownReadPlatformService; - private final ChargeReadPlatformService chargeReadPlatformService; - private final StaffReadPlatformService staffReadPlatformService; - private final DepositsDropdownReadPlatformService depositsDropdownReadPlatformService; - private final SavingsAccountReadPlatformService savingsAccountReadPlatformService; - private final RecurringAccountDepositTransactionTemplateMapper rdTransactionTemplateMapper = new RecurringAccountDepositTransactionTemplateMapper(); - private final DropdownReadPlatformService dropdownReadPlatformService; - private final CalendarReadPlatformService calendarReadPlatformService; - private final PaymentTypeReadService paymentTypeReadPlatformService; - private final AccountNumberGenerator accountNumberGenerator; - private final CodeValueReadPlatformService codeValueReadPlatformService; - - @Override - public Collection retrieveAll(final DepositAccountType depositAccountType, - final PaginationParameters paginationParameters) { - - this.context.authenticatedUser(); - this.paginationParametersDataValidator.validateParameterValues(paginationParameters, supportedOrderByValues, - depositAccountType.resourceName()); - - final DepositAccountMapper depositAccountMapper = this.getDepositAccountMapper(depositAccountType); - if (depositAccountMapper == null) { - return null; - } - - final StringBuilder sqlBuilder = new StringBuilder(400); - sqlBuilder.append("select "); - sqlBuilder.append(depositAccountMapper.schema()); - sqlBuilder.append(" where sa.deposit_type_enum = ? "); - // always append at the end of a sql statement - sqlBuilder.append(paginationParameters.paginationSql()); - - return jdbcTemplate.query(sqlBuilder.toString(), depositAccountMapper, new Object[] { depositAccountType.getValue() }); // NOSONAR - } - - @Override - public Page retrieveAllPaged(final DepositAccountType depositAccountType, - final PaginationParameters paginationParameters) { - - this.paginationParametersDataValidator.validateParameterValues(paginationParameters, supportedOrderByValues, - depositAccountType.resourceName()); - - final DepositAccountMapper depositAccountMapper = this.getDepositAccountMapper(depositAccountType); - if (depositAccountMapper == null) { - return null; - } - - final StringBuilder sqlBuilder = new StringBuilder(400); - sqlBuilder.append("select " + sqlGenerator.calcFoundRows() + " "); - sqlBuilder.append(depositAccountMapper.schema()); - sqlBuilder.append(" where sa.deposit_type_enum = ? "); - sqlBuilder.append(paginationParameters.paginationSql()); - - return this.paginationHelper.fetchPage(this.jdbcTemplate, sqlBuilder.toString(), new Object[] { depositAccountType.getValue() }, - depositAccountMapper); - } - - @Override - public Collection retrieveAllForLookup(final DepositAccountType depositAccountType) { - - final StringBuilder sqlBuilder = new StringBuilder(400); - sqlBuilder.append("select "); - sqlBuilder.append(DEPOSIT_ACCOUNT_LOOKUP_MAPPER.schema()); - sqlBuilder.append(" where sa.deposit_type_enum = ? "); - - return this.jdbcTemplate.query(sqlBuilder.toString(), DEPOSIT_ACCOUNT_LOOKUP_MAPPER, depositAccountType.getValue()); - } - - @Override - public Collection retrieveForMaturityUpdate() { - - final StringBuilder sqlBuilder = new StringBuilder(200); - sqlBuilder.append("SELECT "); - sqlBuilder.append(DEPOSIT_ACCOUNT_FOR_MATURITY_MAPPER.schema()); - sqlBuilder.append(" WHERE da.deposit_type_enum in (?, ?) and da.status_enum = ?"); - - return this.jdbcTemplate.query(sqlBuilder.toString(), DEPOSIT_ACCOUNT_FOR_MATURITY_MAPPER, - DepositAccountType.FIXED_DEPOSIT.getValue(), DepositAccountType.RECURRING_DEPOSIT.getValue(), - SavingsAccountStatusType.ACTIVE.getValue()); - } - - @Override - public DepositAccountData retrieveOne(final DepositAccountType depositAccountType, final Long accountId) { - try { - this.context.authenticatedUser(); - - final DepositAccountMapper depositAccountMapper = this.getDepositAccountMapper(depositAccountType); - if (depositAccountMapper == null) { - return null; - } - - final StringBuilder sqlBuilder = new StringBuilder(400); - sqlBuilder.append("select "); - sqlBuilder.append(depositAccountMapper.schema()); - sqlBuilder.append(" where sa.id = ? and sa.deposit_type_enum = ? "); - - return this.jdbcTemplate.queryForObject(sqlBuilder.toString(), depositAccountMapper, accountId, depositAccountType.getValue()); - - } catch (final EmptyResultDataAccessException e) { - throw new DepositAccountNotFoundException(depositAccountType, accountId, e); - } - } - - @Override - public DepositAccountData retrieveOneWithClosureTemplate(final DepositAccountType depositAccountType, final Long accountId) { - try { - this.context.authenticatedUser(); - - final DepositAccountMapper depositAccountMapper = this.getDepositAccountMapper(depositAccountType); - if (depositAccountMapper == null) { - return null; - } - - final StringBuilder sqlBuilder = new StringBuilder(400); - sqlBuilder.append("select "); - sqlBuilder.append(depositAccountMapper.schema()); - sqlBuilder.append(" where sa.id = ? and sa.deposit_type_enum = ? "); - - DepositAccountData account = this.jdbcTemplate.queryForObject(sqlBuilder.toString(), depositAccountMapper, accountId, - depositAccountType.getValue()); - Collection onAccountClosureOptions = SavingsEnumerations - .depositAccountOnClosureType(DepositAccountOnClosureType.values()); - final Collection paymentTypeOptions = this.paymentTypeReadPlatformService.retrieveAllPaymentTypes(); - final Collection savingsAccountDatas = this.savingsAccountReadPlatformService - .retrieveActiveForLookup(account.getClientId(), DepositAccountType.SAVINGS_DEPOSIT); - if (depositAccountType.isFixedDeposit()) { - account = FixedDepositAccountData.withClosureTemplateDetails((FixedDepositAccountData) account, onAccountClosureOptions, - paymentTypeOptions, savingsAccountDatas); - } else if (depositAccountType.isRecurringDeposit()) { - account = RecurringDepositAccountData.withClosureTemplateDetails((RecurringDepositAccountData) account, - onAccountClosureOptions, paymentTypeOptions, savingsAccountDatas); - } - - return account; - - } catch (final EmptyResultDataAccessException e) { - throw new DepositAccountNotFoundException(depositAccountType, accountId, e); - } - } - - @Override - public DepositAccountData retrieveOneWithChartSlabs(final DepositAccountType depositAccountType, Long accountId) { - DepositAccountData depositAccount = this.retrieveOne(depositAccountType, accountId); - DepositAccountInterestRateChartData chart = this.accountChartReadPlatformService.retrieveOneWithSlabsOnAccountId(accountId); - - if (depositAccountType.isFixedDeposit()) { - depositAccount = FixedDepositAccountData.withInterestChart((FixedDepositAccountData) depositAccount, chart); - } else if (depositAccountType.isRecurringDeposit()) { - CalendarData calendar = this.calendarReadPlatformService.retrieveCollctionCalendarByEntity(accountId, - CalendarEntityType.SAVINGS.getValue()); - Integer frequency = 1; - EnumOptionData frequencyType = null; - if (calendar != null) { - frequency = calendar.getInterval() == -1 ? 1 : calendar.getInterval(); - final CalendarFrequencyType calendarFrequencyType = CalendarFrequencyType - .fromInt(calendar.getFrequency().getId().intValue()); - final PeriodFrequencyType periodFrequencyType = CalendarFrequencyType.from(calendarFrequencyType); - frequencyType = CommonEnumerations.termFrequencyType(periodFrequencyType, "recurring.deposit.frequency."); - } - depositAccount = RecurringDepositAccountData.withInterestChartAndRecurringDetails((RecurringDepositAccountData) depositAccount, - chart, frequency, frequencyType); - } - - return depositAccount; - } - - @Override - public Collection retrieveAllTransactions(final DepositAccountType depositAccountType, - final Long accountId) { - - final String sql = "select " + this.transactionsMapper.schema() - + " where sa.id = ? and sa.deposit_type_enum = ? order by tr.transaction_date DESC, tr.id DESC"; - - return this.jdbcTemplate.query(sql, this.transactionsMapper, new Object[] { accountId, depositAccountType.getValue() }); // NOSONAR - } - - @Override - public DepositAccountData retrieveTemplate(final DepositAccountType depositAccountType, final Long clientId, final Long groupId, - final Long productId, final boolean staffInSelectedOfficeOnly) { - - final AppUser loggedInUser = this.context.authenticatedUser(); - Long officeId = loggedInUser.getOffice().getId(); - - ClientData client = null; - Collection savingsAccountDatas = null; - if (clientId != null) { - client = this.clientReadPlatformService.retrieveOne(clientId); - officeId = client.getOfficeId(); - savingsAccountDatas = this.savingsAccountReadPlatformService.retrieveActiveForLookup(clientId, - DepositAccountType.SAVINGS_DEPOSIT); - } - - GroupGeneralData group = null; - if (groupId != null) { - group = this.groupReadPlatformService.retrieveOne(groupId); - officeId = group.getOfficeId(); - } - - final Collection preClosurePenalInterestOnTypeOptions = this.depositsDropdownReadPlatformService - .retrievePreClosurePenalInterestOnTypeOptions(); - - final Collection periodFrequencyTypeOptions = this.dropdownReadPlatformService.retrievePeriodFrequencyTypeOptions(); - final Collection productOptions = this.depositProductReadPlatformService - .retrieveAllForLookup(depositAccountType); - DepositAccountData template = null; - if (productId != null) { - - final DepositAccountTemplateMapper mapper = getDepositAccountTemplaMapper(depositAccountType, client, group); - - final String sql = "select " + mapper.schema() + " where sa.id = ? and sa.deposit_type_enum = ? "; - template = this.jdbcTemplate.queryForObject(sql, mapper, new Object[] { productId, depositAccountType.getValue() }); // NOSONAR - - final Collection interestCompoundingPeriodTypeOptions = this.savingsDropdownReadPlatformService - .retrieveCompoundingInterestPeriodTypeOptions(); - - final Collection interestPostingPeriodTypeOptions = this.savingsDropdownReadPlatformService - .retrieveInterestPostingPeriodTypeOptions(); - - final Collection interestCalculationTypeOptions = this.savingsDropdownReadPlatformService - .retrieveInterestCalculationTypeOptions(); - - final Collection interestCalculationDaysInYearTypeOptions = this.savingsDropdownReadPlatformService - .retrieveInterestCalculationDaysInYearTypeOptions(); - - final Collection lockinPeriodFrequencyTypeOptions = this.savingsDropdownReadPlatformService - .retrieveLockinPeriodFrequencyTypeOptions(); - - final Collection withdrawalFeeTypeOptions = this.savingsDropdownReadPlatformService - .retrievewithdrawalFeeTypeOptions(); - - final Collection transactions = null; - final Collection productCharges = this.chargeReadPlatformService.retrieveSavingsProductCharges(productId); - // update charges from Product charges - final Collection charges = fromChargesToSavingsCharges(productCharges); - - final boolean feeChargesOnly = false; - final Collection chargeOptions = this.chargeReadPlatformService - .retrieveSavingsProductApplicableCharges(feeChargesOnly); - - final Collection maturityInstructionOptions = this.depositsDropdownReadPlatformService - .maturityInstructionOptions(); - - Collection fieldOfficerOptions = null; - - if (officeId != null) { - - if (staffInSelectedOfficeOnly) { - // only bring back loan officers in selected branch/office - final Collection fieldOfficersInBranch = this.staffReadPlatformService - .retrieveAllLoanOfficersInOfficeById(officeId); - - if (!CollectionUtils.isEmpty(fieldOfficersInBranch)) { - fieldOfficerOptions = new ArrayList<>(fieldOfficersInBranch); - } - } else { - // by default bring back all officers in selected - // branch/office as well as officers in office above - // this office - final boolean restrictToLoanOfficersOnly = true; - final Collection loanOfficersInHierarchy = this.staffReadPlatformService - .retrieveAllStaffInOfficeAndItsParentOfficeHierarchy(officeId, restrictToLoanOfficersOnly); - - if (!CollectionUtils.isEmpty(loanOfficersInHierarchy)) { - fieldOfficerOptions = new ArrayList<>(loanOfficersInHierarchy); - } - } - } - - // retrieve chart Slabs - final InterestRateChartData productChartData = this.productChartReadPlatformService.retrieveActiveChartWithTemplate(productId); - final DepositAccountInterestRateChartData accountChart = (productChartData != null) - ? DepositAccountInterestRateChartData.from(productChartData) - : null; - - if (depositAccountType.isFixedDeposit()) { - - template = FixedDepositAccountData.withTemplateOptions((FixedDepositAccountData) template, productOptions, - fieldOfficerOptions, interestCompoundingPeriodTypeOptions, interestPostingPeriodTypeOptions, - interestCalculationTypeOptions, interestCalculationDaysInYearTypeOptions, lockinPeriodFrequencyTypeOptions, - withdrawalFeeTypeOptions, transactions, charges, chargeOptions, preClosurePenalInterestOnTypeOptions, - periodFrequencyTypeOptions, savingsAccountDatas, maturityInstructionOptions); - - template = FixedDepositAccountData.withInterestChart((FixedDepositAccountData) template, accountChart); - - try { - final Collection accountTypeOpts = this.codeValueReadPlatformService - .retrieveCodeValuesByCode(SavingsApiConstants.FD_PRODUCT_ACCOUNT_TYPE); - final Collection customerSegmentOpts = this.codeValueReadPlatformService - .retrieveCodeValuesByCode(SavingsApiConstants.FD_PRODUCT_CUSTOMER_SEGMENT); - ((FixedDepositAccountData) template).setAccountTypeOptions(accountTypeOpts); - ((FixedDepositAccountData) template).setCustomerSegmentOptions(customerSegmentOpts); - } catch (final Exception ex) { - log.warn("Failed to load FD account type/customer segment options: {}", ex.getMessage()); - } - - try { - final String preview = accountNumberGenerator.previewAccountNumber(productId, officeId); - template.setPreviewAccountNo(preview); - } catch (final Exception ex) { - log.warn("Failed to generate preview account number for FD productId={}: {}", productId, ex.getMessage()); - } - } else if (depositAccountType.isRecurringDeposit()) { - // Save CBS fields from mapper result before factory methods create new objects - final RecurringDepositAccountData rdTemplateWithCbs = (RecurringDepositAccountData) template; - template = RecurringDepositAccountData.withTemplateOptions(rdTemplateWithCbs, productOptions, - fieldOfficerOptions, interestCompoundingPeriodTypeOptions, interestPostingPeriodTypeOptions, - interestCalculationTypeOptions, interestCalculationDaysInYearTypeOptions, lockinPeriodFrequencyTypeOptions, - withdrawalFeeTypeOptions, transactions, charges, chargeOptions, preClosurePenalInterestOnTypeOptions, - periodFrequencyTypeOptions); - template = RecurringDepositAccountData.withInterestChartAndRecurringDetails((RecurringDepositAccountData) template, - accountChart, null, null); - // Re-apply CBS product fields lost during factory method object creation - ((RecurringDepositAccountData) template).copyCbsFieldsFrom(rdTemplateWithCbs); - - // Populate account type / customer segment options from code values - try { - final Collection accountTypeOpts = this.codeValueReadPlatformService - .retrieveCodeValuesByCode(SavingsApiConstants.RD_PRODUCT_ACCOUNT_TYPE); - final Collection customerSegmentOpts = this.codeValueReadPlatformService - .retrieveCodeValuesByCode(SavingsApiConstants.RD_PRODUCT_CUSTOMER_SEGMENT); - ((RecurringDepositAccountData) template).setAccountTypeOptions(accountTypeOpts); - ((RecurringDepositAccountData) template).setCustomerSegmentOptions(customerSegmentOpts); - } catch (final Exception ex) { - log.warn("Failed to load RD account type/customer segment options: {}", ex.getMessage()); - } - - // Generate preview account number - try { - final String preview = accountNumberGenerator.previewAccountNumber(productId, officeId); - template.setPreviewAccountNo(preview); - } catch (final Exception ex) { - log.warn("Failed to generate preview account number for RD productId={}: {}", productId, ex.getMessage()); - } - } - - } else { - - String clientName = null; - if (client != null) { - clientName = client.getDisplayName(); - } - - String groupName = null; - if (group != null) { - groupName = group.getName(); - } - - final Collection fieldOfficerOptions = null; - final Collection interestCompoundingPeriodTypeOptions = null; - final Collection interestPostingPeriodTypeOptions = null; - final Collection interestCalculationTypeOptions = null; - final Collection interestCalculationDaysInYearTypeOptions = null; - final Collection lockinPeriodFrequencyTypeOptions = null; - final Collection withdrawalFeeTypeOptions = null; - - final Collection transactions = null; - final Collection charges = null; - final Collection maturityInstructionOptions = null; - - final boolean feeChargesOnly = true; - final Collection chargeOptions = this.chargeReadPlatformService - .retrieveSavingsProductApplicableCharges(feeChargesOnly); - - if (depositAccountType.isFixedDeposit()) { - - template = FixedDepositAccountData.withClientTemplate(clientId, clientName, groupId, groupName); - - template = FixedDepositAccountData.withTemplateOptions((FixedDepositAccountData) template, productOptions, - fieldOfficerOptions, interestCompoundingPeriodTypeOptions, interestPostingPeriodTypeOptions, - interestCalculationTypeOptions, interestCalculationDaysInYearTypeOptions, lockinPeriodFrequencyTypeOptions, - withdrawalFeeTypeOptions, transactions, charges, chargeOptions, preClosurePenalInterestOnTypeOptions, - periodFrequencyTypeOptions, savingsAccountDatas, maturityInstructionOptions); - } else if (depositAccountType.isRecurringDeposit()) { - - template = RecurringDepositAccountData.withClientTemplate(clientId, clientName, groupId, groupName); - - template = RecurringDepositAccountData.withTemplateOptions((RecurringDepositAccountData) template, productOptions, - fieldOfficerOptions, interestCompoundingPeriodTypeOptions, interestPostingPeriodTypeOptions, - interestCalculationTypeOptions, interestCalculationDaysInYearTypeOptions, lockinPeriodFrequencyTypeOptions, - withdrawalFeeTypeOptions, transactions, charges, chargeOptions, preClosurePenalInterestOnTypeOptions, - periodFrequencyTypeOptions); - } - } - - return template; - } - - @Override - public SavingsAccountTransactionData retrieveRecurringAccountDepositTransactionTemplate(final Long accountId) { - - try { - final String sql = "select " + this.rdTransactionTemplateMapper.schema() - + " where sa.id = ? and sa.deposit_type_enum = ? order by mss.installment limit 1"; - return this.jdbcTemplate.queryForObject(sql, this.rdTransactionTemplateMapper, // NOSONAR - accountId, accountId, DepositAccountType.RECURRING_DEPOSIT.getValue()); - } catch (final EmptyResultDataAccessException e) { - throw new DepositAccountNotFoundException(DepositAccountType.RECURRING_DEPOSIT, accountId, e); - } - } - - @Override - public Collection retrieveDataForInterestTransfer() { - final StringBuilder sqlBuilder = new StringBuilder(300); - AccountTransferMapper mapper = new AccountTransferMapper(); - sqlBuilder.append("SELECT "); - sqlBuilder.append(mapper.schema()); - sqlBuilder.append(" where da.transfer_interest_to_linked_account = true and "); - sqlBuilder.append( - "st.transaction_date > (select coalesce(max(sat.transaction_date),sa.activatedon_date) from m_savings_account_transaction sat where sat.transaction_type_enum = ? and sat.savings_account_id = sa.id and sat.is_reversed=false) "); - sqlBuilder.append( - "and st.transaction_type_enum = ? and sa.status_enum = ? and st.is_reversed=false and st.transaction_date > coalesce(sa.lockedin_until_date_derived,sa.activatedon_date)"); - - return this.jdbcTemplate.query(sqlBuilder.toString(), mapper, SavingsAccountTransactionType.WITHDRAWAL.getValue(), - SavingsAccountTransactionType.INTEREST_POSTING.getValue(), SavingsAccountStatusType.ACTIVE.getValue()); - } - - @Override - public Collection> retriveDataForRDScheduleCreation() { - String today = DATE_TIME_FORMATTER.format(DateUtils.getBusinessLocalDate()); - final StringBuilder sb = new StringBuilder(300); - sb.append(" select rd.savings_account_id savingsId, rd.mandatory_recommended_deposit_amount as amount,"); - sb.append(" mc.recurrence as recurrence ,"); - sb.append(" max(ms.duedate) as dueDate , max(ms.installment) as installment,"); - sb.append(" count(ms.installment) as futureInstallments"); - sb.append(" from m_deposit_account_term_and_preclosure dat "); - sb.append(" inner join m_savings_account sa on sa.id = dat.savings_account_id and sa.status_enum = ?"); - sb.append(" inner join m_deposit_account_recurring_detail rd on rd.savings_account_id = dat.savings_account_id "); - sb.append(" inner join m_calendar_instance mci on mci.entity_type_enum = ? and mci.entity_id = dat.savings_account_id "); - sb.append(" inner join m_calendar mc on mc.id = mci.calendar_id and mc.calendar_type_enum = ?"); - sb.append(" inner join m_mandatory_savings_schedule ms on ms.savings_account_id = dat.savings_account_id and ms.duedate > '" + today - + "'"); - sb.append(" where dat.deposit_period is null"); - sb.append(" group by ms.savings_account_id, rd.mandatory_recommended_deposit_amount, mc.recurrence, rd.savings_account_id"); - - return this.jdbcTemplate.queryForList(sb.toString(), SavingsAccountStatusType.ACTIVE.getValue(), - CalendarEntityType.SAVINGS.getValue(), CalendarType.COLLECTION.getValue()); - } - - private DepositAccountMapper getDepositAccountMapper(final DepositAccountType depositAccountType) { - if (depositAccountType.isFixedDeposit()) { - return FIXED_DEPOSIT_ACCOUNT_MAPPER; - } else if (depositAccountType.isRecurringDeposit()) { - return RECURRING_DEPOSIT_ACCOUNT_MAPPER; - } - return null; - } - - private DepositAccountTemplateMapper getDepositAccountTemplaMapper(final DepositAccountType depositAccountType, ClientData client, - GroupGeneralData group) { - if (depositAccountType.isFixedDeposit()) { - return new FixedDepositAccountTemplateMapper(client, group); - } else if (depositAccountType.isRecurringDeposit()) { - return new RecurringDepositAccountTemplateMapper(client, group); - } - return null; - } - - private Collection fromChargesToSavingsCharges(final Collection productCharges) { - final Collection savingsCharges = new ArrayList<>(); - for (final ChargeData chargeData : productCharges) { - final SavingsAccountChargeData savingsCharge = ConvertChargeDataToSpecificChargeData.toSavingsAccountChargeData(chargeData); - savingsCharges.add(savingsCharge); - } - return savingsCharges; - } - - private abstract static class DepositAccountMapper implements RowMapper { - - public static final String ACCOUNT_NO = "accountNo"; - public static final String ID = "id"; - public static final String EXTERNAL_ID = "externalId"; - public static final String GROUP_ID = "groupId"; - public static final String GROUP_NAME = "groupName"; - public static final String CLIENT_ID = "clientId"; - public static final String CLIENT_NAME = "clientName"; - public static final String PRODUCT_ID = "productId"; - public static final String PRODUCT_NAME = "productName"; - public static final String FIELD_OFFICER_ID = "fieldOfficerId"; - public static final String FIELD_OFFICER_NAME = "fieldOfficerName"; - public static final String STATUS_ENUM = "statusEnum"; - public static final String SUBMITTED_ON_DATE = "submittedOnDate"; - public static final String SUBMITTED_BY_USERNAME = "submittedByUsername"; - public static final String SUBMITTED_BY_LASTNAME = "submittedByLastname"; - public static final String SUBMITTED_BY_FIRSTNAME = "submittedByFirstname"; - public static final String REJECTED_ON_DATE = "rejectedOnDate"; - public static final String REJECTED_BY_USERNAME = "rejectedByUsername"; - public static final String REJECTED_BY_FIRSTNAME = "rejectedByFirstname"; - public static final String REJECTED_BY_LASTNAME = "rejectedByLastname"; - public static final String WITHDRAWN_ON_DATE = "withdrawnOnDate"; - public static final String WITHDRAWN_BY_USERNAME = "withdrawnByUsername"; - public static final String WITHDRAWN_BY_FIRSTNAME = "withdrawnByFirstname"; - public static final String WITHDRAWN_BY_LASTNAME = "withdrawnByLastname"; - public static final String APPROVED_ON_DATE = "approvedOnDate"; - public static final String APPROVED_BY_USERNAME = "approvedByUsername"; - public static final String APPROVED_BY_FIRSTNAME = "approvedByFirstname"; - public static final String APPROVED_BY_LASTNAME = "approvedByLastname"; - public static final String ACTIVATED_ON_DATE = "activatedOnDate"; - public static final String ACTIVATED_BY_USERNAME = "activatedByUsername"; - public static final String ACTIVATED_BY_FIRSTNAME = "activatedByFirstname"; - public static final String ACTIVATED_BY_LASTNAME = "activatedByLastname"; - public static final String CLOSED_ON_DATE = "closedOnDate"; - public static final String CLOSED_BY_USERNAME = "closedByUsername"; - public static final String CLOSED_BY_FIRSTNAME = "closedByFirstname"; - public static final String CLOSED_BY_LASTNAME = "closedByLastname"; - public static final String DEPOSIT_TYPE_ID = "depositTypeId"; - public static final String CURRENCY_CODE = "currencyCode"; - public static final String CURRENCY_NAME = "currencyName"; - public static final String CURRENCY_NAME_CODE = "currencyNameCode"; - public static final String CURRENCY_DISPLAY_SYMBOL = "currencyDisplaySymbol"; - public static final String CURRENCY_DIGITS = "currencyDigits"; - public static final String IN_MULTIPLES_OF = "inMultiplesOf"; - public static final String NOMINAL_ANNUAL_INTEREST_RATE = "nominalAnnualInterestRate"; - public static final String INTEREST_COMPOUNDING_PERIOD_TYPE = "interestCompoundingPeriodType"; - public static final String INTEREST_POSTING_PERIOD_TYPE = "interestPostingPeriodType"; - public static final String INTEREST_CALCULATION_TYPE = "interestCalculationType"; - public static final String INTEREST_CALCULATION_DAYS_IN_YEAR_TYPE = "interestCalculationDaysInYearType"; - public static final String MIN_REQUIRED_OPENING_BALANCE = "minRequiredOpeningBalance"; - public static final String LOCKIN_PERIOD_FREQUENCY = "lockinPeriodFrequency"; - public static final String LOCKIN_PERIOD_FREQUENCY_TYPE = "lockinPeriodFrequencyType"; - public static final String WITHDRAWAL_FEE_FOR_TRANSFERS = "withdrawalFeeForTransfers"; - public static final String MIN_BALANCE_FOR_INTEREST_CALCULATION = "minBalanceForInterestCalculation"; - public static final String TOTAL_DEPOSITS = "totalDeposits"; - public static final String TOTAL_WITHDRAWALS = "totalWithdrawals"; - public static final String TOTAL_WITHDRAWAL_FEES = "totalWithdrawalFees"; - public static final String TOTAL_ANNUAL_FEES = "totalAnnualFees"; - public static final String TOTAL_INTEREST_EARNED = "totalInterestEarned"; - public static final String TOTAL_INTEREST_POSTED = "totalInterestPosted"; - public static final String ACCOUNT_BALANCE = "accountBalance"; - public static final String TOTAL_FEE_CHARGE = "totalFeeCharge"; - public static final String TOTAL_PENALTY_CHARGE = "totalPenaltyCharge"; - public static final String TOTAL_WITHHOLD_TAX = "totalWithholdTax"; - public static final String INTEREST_POSTED_TILL_DATE = "interestPostedTillDate"; - public static final String WITH_HOLD_TAX = "withHoldTax"; - public static final String TAX_GROUP_ID = "taxGroupId"; - public static final String TAX_GROUP_NAME = "taxGroupName"; - private final String selectFieldsSql; - private final String selectTablesSql; - - protected DepositAccountMapper() { - final StringBuilder selectFieldsSqlBuilder = new StringBuilder(400); - selectFieldsSqlBuilder.append("sa.id as id, sa.account_no as accountNo, sa.external_id as externalId, "); - selectFieldsSqlBuilder.append("c.id as clientId, c.display_name as clientName, "); - selectFieldsSqlBuilder.append("g.id as groupId, g.display_name as groupName, "); - selectFieldsSqlBuilder.append("sp.id as productId, sp.name as productName, "); - selectFieldsSqlBuilder.append("s.id fieldOfficerId, s.display_name as fieldOfficerName, "); - selectFieldsSqlBuilder.append("sa.status_enum as statusEnum, "); - selectFieldsSqlBuilder.append("sa.submittedon_date as submittedOnDate,"); - selectFieldsSqlBuilder.append("sbu.username as submittedByUsername,"); - selectFieldsSqlBuilder.append("sbu.firstname as submittedByFirstname, sbu.lastname as submittedByLastname,"); - selectFieldsSqlBuilder.append("sa.rejectedon_date as rejectedOnDate,"); - selectFieldsSqlBuilder.append("rbu.username as rejectedByUsername,"); - selectFieldsSqlBuilder.append("rbu.firstname as rejectedByFirstname, rbu.lastname as rejectedByLastname,"); - selectFieldsSqlBuilder.append("sa.withdrawnon_date as withdrawnOnDate,"); - selectFieldsSqlBuilder.append("wbu.username as withdrawnByUsername,"); - selectFieldsSqlBuilder.append("wbu.firstname as withdrawnByFirstname, wbu.lastname as withdrawnByLastname,"); - selectFieldsSqlBuilder.append("sa.approvedon_date as approvedOnDate,"); - selectFieldsSqlBuilder.append("abu.username as approvedByUsername,"); - selectFieldsSqlBuilder.append("abu.firstname as approvedByFirstname, abu.lastname as approvedByLastname,"); - selectFieldsSqlBuilder.append("sa.activatedon_date as activatedOnDate,"); - selectFieldsSqlBuilder.append("avbu.username as activatedByUsername,"); - selectFieldsSqlBuilder.append("avbu.firstname as activatedByFirstname, avbu.lastname as activatedByLastname,"); - selectFieldsSqlBuilder.append("sa.closedon_date as closedOnDate,"); - selectFieldsSqlBuilder.append("cbu.username as closedByUsername,"); - selectFieldsSqlBuilder.append("cbu.firstname as closedByFirstname, cbu.lastname as closedByLastname,"); - selectFieldsSqlBuilder.append( - "sa.currency_code as currencyCode, sa.currency_digits as currencyDigits, sa.currency_multiplesof as inMultiplesOf, "); - selectFieldsSqlBuilder.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, "); - selectFieldsSqlBuilder.append("curr.display_symbol as currencyDisplaySymbol, "); - selectFieldsSqlBuilder.append("sa.nominal_annual_interest_rate as nominalAnnualInterestRate, "); - selectFieldsSqlBuilder.append("sa.interest_compounding_period_enum as interestCompoundingPeriodType, "); - selectFieldsSqlBuilder.append("sa.interest_posting_period_enum as interestPostingPeriodType, "); - selectFieldsSqlBuilder.append("sa.interest_calculation_type_enum as interestCalculationType, "); - selectFieldsSqlBuilder.append("sa.interest_calculation_days_in_year_type_enum as interestCalculationDaysInYearType, "); - selectFieldsSqlBuilder.append("sa.min_required_opening_balance as minRequiredOpeningBalance, "); - selectFieldsSqlBuilder.append("sa.lockin_period_frequency as lockinPeriodFrequency,"); - selectFieldsSqlBuilder.append("sa.lockin_period_frequency_enum as lockinPeriodFrequencyType, "); - selectFieldsSqlBuilder.append("sa.withdrawal_fee_for_transfer as withdrawalFeeForTransfers, "); - selectFieldsSqlBuilder.append("sa.total_deposits_derived as totalDeposits, "); - selectFieldsSqlBuilder.append("sa.total_withdrawals_derived as totalWithdrawals, "); - selectFieldsSqlBuilder.append("sa.interest_posted_till_date as interestPostedTillDate, "); - selectFieldsSqlBuilder.append("sa.total_withdrawal_fees_derived as totalWithdrawalFees, "); - selectFieldsSqlBuilder.append("sa.total_annual_fees_derived as totalAnnualFees, "); - selectFieldsSqlBuilder.append("sa.total_interest_earned_derived as totalInterestEarned, "); - selectFieldsSqlBuilder.append("sa.total_interest_posted_derived as totalInterestPosted, "); - selectFieldsSqlBuilder.append("sa.account_balance_derived as accountBalance, "); - selectFieldsSqlBuilder.append("sa.total_fees_charge_derived as totalFeeCharge, "); - selectFieldsSqlBuilder.append("sa.total_penalty_charge_derived as totalPenaltyCharge, "); - selectFieldsSqlBuilder.append("sa.total_withhold_tax_derived as totalWithholdTax,"); - selectFieldsSqlBuilder.append("sa.deposit_type_enum as depositTypeId, "); - selectFieldsSqlBuilder.append("sa.cbs_account_type as accountType, sa.customer_segment as customerSegment, "); - selectFieldsSqlBuilder.append("sa.account_status as accountStatus, sa.mode_of_operation as modeOfOperation, "); - selectFieldsSqlBuilder.append("sa.special_category as specialCategory, sa.weaker_section as weakerSection, "); - selectFieldsSqlBuilder.append("sa.guardian_name as guardianName, sa.guardian_cif_number as guardianCifNumber, "); - selectFieldsSqlBuilder.append("sa.guardian_relationship as guardianRelationship, sa.guardian_client_id as guardianClientId, "); - selectFieldsSqlBuilder.append("sa.minor_date_of_birth as minorDateOfBirth, "); - selectFieldsSqlBuilder.append("sa.introducer_client_id as introducerClientId, sa.introducer_name as introducerName, "); - selectFieldsSqlBuilder.append("sa.introducer_cif_number as introducerCifNumber, "); - selectFieldsSqlBuilder.append("sa.linked_account_id as linkedAccountId, sa.linked_account_type as linkedAccountType, "); - selectFieldsSqlBuilder.append("sa.linked_account_no as linkedAccountNo, "); - selectFieldsSqlBuilder.append("sa.min_balance_for_interest_calculation as minBalanceForInterestCalculation, "); - selectFieldsSqlBuilder.append("sa.withhold_tax as withHoldTax,"); - selectFieldsSqlBuilder.append("tg.id as taxGroupId, tg.name as taxGroupName "); - - this.selectFieldsSql = selectFieldsSqlBuilder.toString(); - - final StringBuilder selectTablesSqlBuilder = new StringBuilder(400); - selectTablesSqlBuilder.append("from m_savings_account sa "); - selectTablesSqlBuilder.append("left join m_deposit_account_term_and_preclosure datp on sa.id = datp.savings_account_id "); - selectTablesSqlBuilder.append("join m_savings_product sp ON sa.product_id = sp.id "); - selectTablesSqlBuilder.append("left join m_code_value icv on icv.id = sp.interest_calculate_selector "); - selectTablesSqlBuilder.append("join m_currency curr on curr.code = sa.currency_code "); - selectTablesSqlBuilder.append("left join m_client c ON c.id = sa.client_id "); - selectTablesSqlBuilder.append("left join m_group g ON g.id = sa.group_id "); - selectTablesSqlBuilder.append("left join m_staff s ON s.id = sa.field_officer_id "); - selectTablesSqlBuilder.append("left join m_appuser sbu on sbu.id = sa.submittedon_userid "); - selectTablesSqlBuilder.append("left join m_appuser rbu on rbu.id = sa.rejectedon_userid "); - selectTablesSqlBuilder.append("left join m_appuser wbu on wbu.id = sa.withdrawnon_userid "); - selectTablesSqlBuilder.append("left join m_appuser abu on abu.id = sa.approvedon_userid "); - selectTablesSqlBuilder.append("left join m_appuser avbu on avbu.id = sa.activatedon_userid "); - selectTablesSqlBuilder.append("left join m_appuser cbu on cbu.id = sa.closedon_userid "); - selectTablesSqlBuilder.append("left join m_tax_group tg on tg.id = sa.tax_group_id "); - - this.selectTablesSql = selectTablesSqlBuilder.toString(); - } - - public String selectFieldsSql() { - return this.selectFieldsSql; - } - - public String selectTablesSql() { - return this.selectTablesSql; - } - - public abstract String schema(); - - public DepositAccountData mapRow(final ResultSet rs) throws SQLException { - - final Long id = rs.getLong(ID); - final String accountNo = rs.getString(ACCOUNT_NO); - final String externalId = rs.getString(EXTERNAL_ID); - - final Long groupId = JdbcSupport.getLong(rs, GROUP_ID); - final String groupName = rs.getString(GROUP_NAME); - final Long clientId = JdbcSupport.getLong(rs, CLIENT_ID); - final String clientName = rs.getString(CLIENT_NAME); - - final Long productId = rs.getLong(PRODUCT_ID); - final String productName = rs.getString(PRODUCT_NAME); - - final Long fieldOfficerId = rs.getLong(FIELD_OFFICER_ID); - final String fieldOfficerName = rs.getString(FIELD_OFFICER_NAME); - - final Integer statusEnum = JdbcSupport.getInteger(rs, STATUS_ENUM); - final SavingsAccountStatusEnumData status = SavingsEnumerations.status(statusEnum); - - final LocalDate submittedOnDate = JdbcSupport.getLocalDate(rs, SUBMITTED_ON_DATE); - final String submittedByUsername = rs.getString(SUBMITTED_BY_USERNAME); - final String submittedByFirstname = rs.getString(SUBMITTED_BY_FIRSTNAME); - final String submittedByLastname = rs.getString(SUBMITTED_BY_LASTNAME); - - final LocalDate rejectedOnDate = JdbcSupport.getLocalDate(rs, REJECTED_ON_DATE); - final String rejectedByUsername = rs.getString(REJECTED_BY_USERNAME); - final String rejectedByFirstname = rs.getString(REJECTED_BY_FIRSTNAME); - final String rejectedByLastname = rs.getString(REJECTED_BY_LASTNAME); - - final LocalDate withdrawnOnDate = JdbcSupport.getLocalDate(rs, WITHDRAWN_ON_DATE); - final String withdrawnByUsername = rs.getString(WITHDRAWN_BY_USERNAME); - final String withdrawnByFirstname = rs.getString(WITHDRAWN_BY_FIRSTNAME); - final String withdrawnByLastname = rs.getString(WITHDRAWN_BY_LASTNAME); - - final LocalDate approvedOnDate = JdbcSupport.getLocalDate(rs, APPROVED_ON_DATE); - final String approvedByUsername = rs.getString(APPROVED_BY_USERNAME); - final String approvedByFirstname = rs.getString(APPROVED_BY_FIRSTNAME); - final String approvedByLastname = rs.getString(APPROVED_BY_LASTNAME); - - final LocalDate activatedOnDate = JdbcSupport.getLocalDate(rs, ACTIVATED_ON_DATE); - final String activatedByUsername = rs.getString(ACTIVATED_BY_USERNAME); - final String activatedByFirstname = rs.getString(ACTIVATED_BY_FIRSTNAME); - final String activatedByLastname = rs.getString(ACTIVATED_BY_LASTNAME); - - final LocalDate closedOnDate = JdbcSupport.getLocalDate(rs, CLOSED_ON_DATE); - final String closedByUsername = rs.getString(CLOSED_BY_USERNAME); - final String closedByFirstname = rs.getString(CLOSED_BY_FIRSTNAME); - final String closedByLastname = rs.getString(CLOSED_BY_LASTNAME); - - final SavingsAccountApplicationTimelineData timeline = new SavingsAccountApplicationTimelineData(submittedOnDate, - submittedByUsername, submittedByFirstname, submittedByLastname, rejectedOnDate, rejectedByUsername, rejectedByFirstname, - rejectedByLastname, withdrawnOnDate, withdrawnByUsername, withdrawnByFirstname, withdrawnByLastname, approvedOnDate, - approvedByUsername, approvedByFirstname, approvedByLastname, activatedOnDate, activatedByUsername, activatedByFirstname, - activatedByLastname, closedOnDate, closedByUsername, closedByFirstname, closedByLastname); - - final Integer depositTypeId = JdbcSupport.getInteger(rs, DEPOSIT_TYPE_ID); - final EnumOptionData depositType = (depositTypeId == null) ? null : SavingsEnumerations.depositType(depositTypeId); - - final String currencyCode = rs.getString(CURRENCY_CODE); - final String currencyName = rs.getString(CURRENCY_NAME); - final String currencyNameCode = rs.getString(CURRENCY_NAME_CODE); - final String currencyDisplaySymbol = rs.getString(CURRENCY_DISPLAY_SYMBOL); - final Integer currencyDigits = JdbcSupport.getInteger(rs, CURRENCY_DIGITS); - final Integer inMultiplesOf = JdbcSupport.getInteger(rs, IN_MULTIPLES_OF); - final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMultiplesOf, currencyDisplaySymbol, - currencyNameCode); - - final BigDecimal nominalAnnualInterestRate = rs.getBigDecimal(NOMINAL_ANNUAL_INTEREST_RATE); - - final EnumOptionData interestCompoundingPeriodType = SavingsEnumerations.compoundingInterestPeriodType( - SavingsCompoundingInterestPeriodType.fromInt(JdbcSupport.getInteger(rs, INTEREST_COMPOUNDING_PERIOD_TYPE))); - - final EnumOptionData interestPostingPeriodType = SavingsEnumerations.interestPostingPeriodType( - SavingsPostingInterestPeriodType.fromInt(JdbcSupport.getInteger(rs, INTEREST_POSTING_PERIOD_TYPE))); - - final EnumOptionData interestCalculationType = SavingsEnumerations - .interestCalculationType(SavingsInterestCalculationType.fromInt(JdbcSupport.getInteger(rs, INTEREST_CALCULATION_TYPE))); - - final EnumOptionData interestCalculationDaysInYearType = SavingsEnumerations.interestCalculationDaysInYearType( - SavingsInterestCalculationDaysInYearType.fromInt(JdbcSupport.getInteger(rs, INTEREST_CALCULATION_DAYS_IN_YEAR_TYPE))); - - final BigDecimal minRequiredOpeningBalance = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, MIN_REQUIRED_OPENING_BALANCE); - - final Integer lockinPeriodFrequency = JdbcSupport.getInteger(rs, LOCKIN_PERIOD_FREQUENCY); - EnumOptionData lockinPeriodFrequencyType = null; - final Integer lockinPeriodFrequencyTypeValue = JdbcSupport.getInteger(rs, LOCKIN_PERIOD_FREQUENCY_TYPE); - if (lockinPeriodFrequencyTypeValue != null) { - final SavingsPeriodFrequencyType lockinPeriodType = SavingsPeriodFrequencyType.fromInt(lockinPeriodFrequencyTypeValue); - lockinPeriodFrequencyType = SavingsEnumerations.lockinPeriodFrequencyType(lockinPeriodType); - } - - final boolean withdrawalFeeForTransfers = rs.getBoolean(WITHDRAWAL_FEE_FOR_TRANSFERS); - final BigDecimal minBalanceForInterestCalculation = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, - MIN_BALANCE_FOR_INTEREST_CALCULATION); - - final BigDecimal totalDeposits = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, TOTAL_DEPOSITS); - final BigDecimal totalWithdrawals = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, TOTAL_WITHDRAWALS); - final BigDecimal totalWithdrawalFees = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, TOTAL_WITHDRAWAL_FEES); - final BigDecimal totalAnnualFees = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, TOTAL_ANNUAL_FEES); - - final BigDecimal totalInterestEarned = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, TOTAL_INTEREST_EARNED); - final BigDecimal totalInterestPosted = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, TOTAL_INTEREST_POSTED); - final BigDecimal accountBalance = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, ACCOUNT_BALANCE); - final BigDecimal totalFeeCharge = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, TOTAL_FEE_CHARGE); - final BigDecimal totalPenaltyCharge = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, TOTAL_PENALTY_CHARGE); - final BigDecimal totalWithholdTax = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, TOTAL_WITHHOLD_TAX); - final BigDecimal totalOverdraftInterestDerived = null; - final LocalDate interestPostedTillDate = JdbcSupport.getLocalDate(rs, INTEREST_POSTED_TILL_DATE); - - final boolean withHoldTax = rs.getBoolean(WITH_HOLD_TAX); - final Long taxGroupId = JdbcSupport.getLong(rs, TAX_GROUP_ID); - final String taxGroupName = rs.getString(TAX_GROUP_NAME); - TaxGroupData taxGroupData = null; - if (taxGroupId != null) { - taxGroupData = TaxGroupData.lookup(taxGroupId, taxGroupName); - } - - final BigDecimal availableBalance = null; - final SavingsAccountSummaryData summary = new SavingsAccountSummaryData(currency, totalDeposits, totalWithdrawals, - totalWithdrawalFees, totalAnnualFees, totalInterestEarned, totalInterestPosted, accountBalance, totalFeeCharge, - totalPenaltyCharge, totalOverdraftInterestDerived, totalWithholdTax, null, null, availableBalance, - interestPostedTillDate); - - return DepositAccountData.instance(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName, - fieldOfficerId, fieldOfficerName, status, timeline, currency, nominalAnnualInterestRate, interestCompoundingPeriodType, - interestPostingPeriodType, interestCalculationType, interestCalculationDaysInYearType, minRequiredOpeningBalance, - lockinPeriodFrequency, lockinPeriodFrequencyType, withdrawalFeeForTransfers, summary, depositType, - minBalanceForInterestCalculation, withHoldTax, taxGroupData); - } - } - - private static class FixedDepositAccountMapper extends DepositAccountMapper { - - public static final String PRE_CLOSURE_PENAL_APPLICABLE = "preClosurePenalApplicable"; - public static final String PRE_CLOSURE_PENAL_INTEREST = "preClosurePenalInterest"; - public static final String PRE_CLOSURE_PENAL_INTEREST_ON_ID = "preClosurePenalInterestOnId"; - public static final String MIN_DEPOSIT_TERM = "minDepositTerm"; - public static final String MAX_DEPOSIT_TERM = "maxDepositTerm"; - public static final String MIN_DEPOSIT_TERM_TYPE_ID = "minDepositTermTypeId"; - public static final String MAX_DEPOSIT_TERM_TYPE_ID = "maxDepositTermTypeId"; - public static final String IN_MULTIPLES_OF_DEPOSIT_TERM = "inMultiplesOfDepositTerm"; - public static final String IN_MULTIPLES_OF_DEPOSIT_TERM_TYPE_ID = "inMultiplesOfDepositTermTypeId"; - public static final String DEPOSIT_AMOUNT = "depositAmount"; - public static final String MATURITY_AMOUNT = "maturityAmount"; - public static final String MATURITY_DATE = "maturityDate"; - public static final String DEPOSIT_PERIOD = "depositPeriod"; - public static final String DEPOSIT_PERIOD_FREQUENCY_TYPE_ID = "depositPeriodFrequencyTypeId"; - public static final String ON_ACCOUNT_CLOSURE_ID = "onAccountClosureId"; - public static final String TRANSFER_INTEREST_TO_SAVINGS = "transferInterestToSavings"; - public static final String TRANSFER_TO_SAVINGS_ID = "transferToSavingsId"; - private final String schemaSql; - - FixedDepositAccountMapper() { - final StringBuilder sqlBuilder = new StringBuilder(400); - sqlBuilder.append(super.selectFieldsSql()); - - sqlBuilder.append(", datp.pre_closure_penal_applicable as preClosurePenalApplicable, "); - sqlBuilder.append("datp.pre_closure_penal_interest as preClosurePenalInterest, "); - sqlBuilder.append("datp.pre_closure_penal_interest_on_enum as preClosurePenalInterestOnId, "); - sqlBuilder.append("datp.min_deposit_term as minDepositTerm, "); - sqlBuilder.append("datp.max_deposit_term as maxDepositTerm, "); - sqlBuilder.append("datp.min_deposit_term_type_enum as minDepositTermTypeId, "); - sqlBuilder.append("datp.max_deposit_term_type_enum as maxDepositTermTypeId, "); - sqlBuilder.append("datp.in_multiples_of_deposit_term as inMultiplesOfDepositTerm, "); - sqlBuilder.append("datp.in_multiples_of_deposit_term_type_enum as inMultiplesOfDepositTermTypeId, "); - sqlBuilder.append("datp.deposit_amount as depositAmount, "); - sqlBuilder.append("datp.maturity_amount as maturityAmount, "); - sqlBuilder.append("datp.maturity_date as maturityDate, "); - sqlBuilder.append("datp.deposit_period as depositPeriod, "); - sqlBuilder.append("datp.deposit_period_frequency_enum as depositPeriodFrequencyTypeId, "); - sqlBuilder.append("datp.on_account_closure_enum as onAccountClosureId, "); - sqlBuilder.append("datp.transfer_interest_to_linked_account as transferInterestToSavings, "); - sqlBuilder.append("datp.transfer_to_savings_account_id as transferToSavingsId, "); - sqlBuilder.append("sp.fd_type as fdType, sp.product_code as productCode, sp.effective_date as effectiveDate, "); - sqlBuilder.append("sp.effective_to as effectiveTo, sp.end_date as endDate, sp.introducer_required as introducerRequired, "); - sqlBuilder.append("sp.share_linking_enabled as shareLinkingEnabled, "); - sqlBuilder.append("COALESCE(sa.nomination_facility, sp.nomination_facility) as nominationFacility, "); - sqlBuilder.append("sp.dormancy_alert as dormancyAlert, sp.is_dormancy_tracking_active as isDormancyTrackingActive, "); - sqlBuilder.append("sp.days_to_inactive as daysToInactive, sp.days_to_dormancy as daysToDormancy, "); - sqlBuilder.append("sp.days_to_escheat as daysToEscheat, sp.interest_calculate_selector as interestCalculateSelector, "); - sqlBuilder.append("icv.code_value as interestCalculationTypeName, sp.average_period as averagePeriod, "); - sqlBuilder.append("sp.auto_post as autoPost, sp.frequency_fix_day as frequencyFixDay, "); - sqlBuilder.append("sp.last_interest_date as lastInterestDate, sp.enable_lockin_period as enableLockinPeriod, "); - sqlBuilder.append("sp.balance_frequency as balanceFrequency, "); - sqlBuilder.append("sp.account_closed_zero_balance_in_day_closing as accountClosedZeroBalanceInDayClosing, "); - sqlBuilder.append("sp.balance_swap_option as balanceSwapOption, sp.msme as msme, "); - sqlBuilder.append("COALESCE(sa.auto_debit_facility, sp.auto_debit_facility) as autoDebitFacility, "); - sqlBuilder.append("sp.loan_facility as loanFacility, sp.premature_withdrawal_option as prematureWithdrawalOption, "); - sqlBuilder.append("sp.tenure_flexibility as tenureFlexibility, sp.lien_marked as lienMarked, "); - sqlBuilder.append("sp.maturity_instructions as maturityInstructions, sp.credit_maturity_proceeds as creditMaturityProceeds, "); - sqlBuilder.append("sp.penalty_grace_period_missed_installments as penaltyGracePeriodMissedInstallments, "); - sqlBuilder.append("sp.panel_request_applicable as panelRequestApplicable, "); - sqlBuilder.append("sp.no_penalty_nominee_heir_closure as noPenaltyNomineeHeirClosure, "); - sqlBuilder.append("sp.deceased_settlement as deceasedSettlement, sp.allow_debit_txn_after_due_date as allowDebitTxnAfterDueDate "); - - sqlBuilder.append(super.selectTablesSql()); - - this.schemaSql = sqlBuilder.toString(); - } - - @Override - public String schema() { - return this.schemaSql; - } - - @Override - public FixedDepositAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { - - final DepositAccountData depositAccountData = super.mapRow(rs); - final boolean preClosurePenalApplicable = rs.getBoolean(PRE_CLOSURE_PENAL_APPLICABLE); - final BigDecimal preClosurePenalInterest = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, PRE_CLOSURE_PENAL_INTEREST); - final Integer preClosurePenalInterestOnTypeId = JdbcSupport.getInteger(rs, PRE_CLOSURE_PENAL_INTEREST_ON_ID); - final EnumOptionData preClosurePenalInterestOnType = (preClosurePenalInterestOnTypeId == null) ? null - : SavingsEnumerations.preClosurePenaltyInterestOnType(preClosurePenalInterestOnTypeId); - - final Integer minDepositTerm = JdbcSupport.getInteger(rs, MIN_DEPOSIT_TERM); - final Integer maxDepositTerm = JdbcSupport.getInteger(rs, MAX_DEPOSIT_TERM); - final Integer minDepositTermTypeId = JdbcSupport.getInteger(rs, MIN_DEPOSIT_TERM_TYPE_ID); - final EnumOptionData minDepositTermType = (minDepositTermTypeId == null) ? null - : SavingsEnumerations.depositTermFrequencyType(minDepositTermTypeId); - final Integer maxDepositTermTypeId = JdbcSupport.getInteger(rs, MAX_DEPOSIT_TERM_TYPE_ID); - final EnumOptionData maxDepositTermType = (maxDepositTermTypeId == null) ? null - : SavingsEnumerations.depositTermFrequencyType(maxDepositTermTypeId); - final Integer inMultiplesOfDepositTerm = JdbcSupport.getInteger(rs, IN_MULTIPLES_OF_DEPOSIT_TERM); - final Integer inMultiplesOfDepositTermTypeId = JdbcSupport.getInteger(rs, IN_MULTIPLES_OF_DEPOSIT_TERM_TYPE_ID); - final EnumOptionData inMultiplesOfDepositTermType = (inMultiplesOfDepositTermTypeId == null) ? null - : SavingsEnumerations.depositTermFrequencyType(inMultiplesOfDepositTermTypeId); - - final BigDecimal depositAmount = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, DEPOSIT_AMOUNT); - final BigDecimal maturityAmount = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, MATURITY_AMOUNT); - final LocalDate maturityDate = JdbcSupport.getLocalDate(rs, MATURITY_DATE); - final Integer depositPeriod = JdbcSupport.getInteger(rs, DEPOSIT_PERIOD); - final Integer depositPeriodFrequencyTypeId = JdbcSupport.getInteger(rs, DEPOSIT_PERIOD_FREQUENCY_TYPE_ID); - final EnumOptionData depositPeriodFrequencyType = (depositPeriodFrequencyTypeId == null) ? null - : SavingsEnumerations.depositPeriodFrequency(depositPeriodFrequencyTypeId); - - final Integer onAccountClosureId = JdbcSupport.getInteger(rs, ON_ACCOUNT_CLOSURE_ID); - final EnumOptionData onAccountClosureType = (onAccountClosureId == null) ? null - : SavingsEnumerations.depositAccountOnClosureType(onAccountClosureId); - final Boolean transferInterestToSavings = rs.getBoolean(TRANSFER_INTEREST_TO_SAVINGS); - - final Long transferToSavingsId = JdbcSupport.getLong(rs, TRANSFER_TO_SAVINGS_ID); - - final FixedDepositAccountData result = FixedDepositAccountData.instance(depositAccountData, preClosurePenalApplicable, preClosurePenalInterest, - preClosurePenalInterestOnType, minDepositTerm, maxDepositTerm, minDepositTermType, maxDepositTermType, - inMultiplesOfDepositTerm, inMultiplesOfDepositTermType, depositAmount, maturityAmount, maturityDate, depositPeriod, - depositPeriodFrequencyType, onAccountClosureType, transferInterestToSavings, transferToSavingsId); - result.setCbsProductFields(rs.getString("accountType"), rs.getString("customerSegment"), rs.getString("fdType"), rs.getString("productCode"), - JdbcSupport.getLocalDate(rs, "effectiveDate"), JdbcSupport.getLocalDate(rs, "effectiveTo"), - JdbcSupport.getLocalDate(rs, "endDate"), rs.getBoolean("introducerRequired"), rs.getBoolean("shareLinkingEnabled"), - rs.getBoolean("nominationFacility"), rs.getBoolean("dormancyAlert"), rs.getBoolean("isDormancyTrackingActive"), - JdbcSupport.getLong(rs, "daysToInactive"), JdbcSupport.getLong(rs, "daysToDormancy"), - JdbcSupport.getLong(rs, "daysToEscheat"), JdbcSupport.getInteger(rs, "interestCalculateSelector"), - rs.getString("interestCalculationTypeName"), rs.getString("averagePeriod"), rs.getBoolean("autoPost"), - JdbcSupport.getInteger(rs, "frequencyFixDay"), JdbcSupport.getLocalDate(rs, "lastInterestDate"), - rs.getBoolean("enableLockinPeriod"), rs.getString("balanceFrequency"), - rs.getBoolean("accountClosedZeroBalanceInDayClosing"), rs.getBoolean("balanceSwapOption"), rs.getBoolean("msme"), - rs.getBoolean("autoDebitFacility"), rs.getBoolean("loanFacility"), rs.getBoolean("prematureWithdrawalOption"), - rs.getBoolean("tenureFlexibility"), rs.getBoolean("lienMarked"), rs.getBoolean("maturityInstructions"), - rs.getBoolean("creditMaturityProceeds"), rs.getBoolean("penaltyGracePeriodMissedInstallments"), - rs.getBoolean("panelRequestApplicable"), rs.getBoolean("noPenaltyNomineeHeirClosure"), - rs.getBoolean("deceasedSettlement"), rs.getBoolean("allowDebitTxnAfterDueDate")); - return result; - } - } - - private static class RecurringDepositAccountMapper extends DepositAccountMapper { - - public static final String PRE_CLOSURE_PENAL_APPLICABLE = "preClosurePenalApplicable"; - public static final String PRE_CLOSURE_PENAL_INTEREST = "preClosurePenalInterest"; - public static final String PRE_CLOSURE_PENAL_INTEREST_ON_ID = "preClosurePenalInterestOnId"; - public static final String MIN_DEPOSIT_TERM = "minDepositTerm"; - public static final String MAX_DEPOSIT_TERM = "maxDepositTerm"; - public static final String MIN_DEPOSIT_TERM_TYPE_ID = "minDepositTermTypeId"; - public static final String MAX_DEPOSIT_TERM_TYPE_ID = "maxDepositTermTypeId"; - public static final String IN_MULTIPLES_OF_DEPOSIT_TERM = "inMultiplesOfDepositTerm"; - public static final String IN_MULTIPLES_OF_DEPOSIT_TERM_TYPE_ID = "inMultiplesOfDepositTermTypeId"; - public static final String DEPOSIT_AMOUNT = "depositAmount"; - public static final String MATURITY_AMOUNT = "maturityAmount"; - public static final String MATURITY_DATE = "maturityDate"; - public static final String DEPOSIT_PERIOD = "depositPeriod"; - public static final String DEPOSIT_PERIOD_FREQUENCY_TYPE_ID = "depositPeriodFrequencyTypeId"; - public static final String MANDATORY_RECOMMENDED_DEPOSIT_AMOUNT = "mandatoryRecommendedDepositAmount"; - public static final String TOTAL_OVERDUE_AMOUNT = "totalOverdueAmount"; - public static final String NO_OF_OVERDUE_INSTALLMENTS = "noOfOverdueInstallments"; - public static final String IS_MANDATORY_DEPOSIT = "isMandatoryDeposit"; - public static final String ALLOW_WITHDRAWAL = "allowWithdrawal"; - public static final String ADJUST_ADVANCE_TOWARDS_FUTURE_PAYMENTS = "adjustAdvanceTowardsFuturePayments"; - public static final String IS_CALENDAR_INHERITED = "isCalendarInherited"; - public static final String ON_ACCOUNT_CLOSURE_ID = "onAccountClosureId"; - public static final String EXPECTED_FIRST_DEPOSIT_ON_DATE = "expectedFirstDepositOnDate"; - private final String schemaSql; - - RecurringDepositAccountMapper() { - final StringBuilder sqlBuilder = new StringBuilder(400); - sqlBuilder.append(this.selectFieldsSql()); - - sqlBuilder.append(", datp.pre_closure_penal_applicable as preClosurePenalApplicable, "); - sqlBuilder.append("datp.pre_closure_penal_interest as preClosurePenalInterest, "); - sqlBuilder.append("datp.pre_closure_penal_interest_on_enum as preClosurePenalInterestOnId, "); - sqlBuilder.append("dard.mandatory_recommended_deposit_amount as mandatoryRecommendedDepositAmount, "); - sqlBuilder.append("dard.total_overdue_amount as totalOverdueAmount, "); - sqlBuilder.append("dard.no_of_overdue_installments as noOfOverdueInstallments, "); - sqlBuilder.append("dard.is_mandatory as isMandatoryDeposit, "); - sqlBuilder.append("dard.allow_withdrawal as allowWithdrawal, "); - sqlBuilder.append("dard.adjust_advance_towards_future_payments as adjustAdvanceTowardsFuturePayments, "); - sqlBuilder.append("dard.is_calendar_inherited as isCalendarInherited, "); - sqlBuilder.append("datp.min_deposit_term as minDepositTerm, "); - sqlBuilder.append("datp.max_deposit_term as maxDepositTerm, "); - sqlBuilder.append("datp.min_deposit_term_type_enum as minDepositTermTypeId, "); - sqlBuilder.append("datp.max_deposit_term_type_enum as maxDepositTermTypeId, "); - sqlBuilder.append("datp.in_multiples_of_deposit_term as inMultiplesOfDepositTerm, "); - sqlBuilder.append("datp.in_multiples_of_deposit_term_type_enum as inMultiplesOfDepositTermTypeId, "); - sqlBuilder.append("datp.deposit_amount as depositAmount, "); - sqlBuilder.append("datp.maturity_amount as maturityAmount, "); - sqlBuilder.append("datp.expected_firstdepositon_date as expectedFirstDepositOnDate, "); - sqlBuilder.append("datp.maturity_date as maturityDate, "); - sqlBuilder.append("datp.deposit_period as depositPeriod, "); - sqlBuilder.append("datp.deposit_period_frequency_enum as depositPeriodFrequencyTypeId, "); - sqlBuilder.append("datp.on_account_closure_enum as onAccountClosureId, "); - sqlBuilder.append("sp.product_code as productCode, sp.effective_date as effectiveDate, "); - sqlBuilder.append("sp.effective_to as effectiveTo, sp.end_date as endDate, sp.introducer_required as introducerRequired, "); - sqlBuilder.append("sp.share_linking_enabled as shareLinkingEnabled, "); - sqlBuilder.append("COALESCE(sa.nomination_facility, sp.nomination_facility) as nominationFacility, "); - sqlBuilder.append("sp.dormancy_alert as dormancyAlert, sp.is_dormancy_tracking_active as isDormancyTrackingActive, "); - sqlBuilder.append("sp.days_to_inactive as daysToInactive, sp.days_to_dormancy as daysToDormancy, "); - sqlBuilder.append("sp.days_to_escheat as daysToEscheat, sp.interest_calculate_selector as interestCalculateSelector, "); - sqlBuilder.append("icv.code_value as interestCalculationTypeName, sp.average_period as averagePeriod, "); - sqlBuilder.append("sp.auto_post as autoPost, sp.frequency_fix_day as frequencyFixDay, "); - sqlBuilder.append("sp.last_interest_date as lastInterestDate, sp.enable_lockin_period as enableLockinPeriod, "); - sqlBuilder.append("sp.balance_frequency as balanceFrequency, "); - sqlBuilder.append("sp.account_closed_zero_balance_in_day_closing as accountClosedZeroBalanceInDayClosing, "); - sqlBuilder.append("sp.balance_swap_option as balanceSwapOption, sp.msme as msme, "); - sqlBuilder.append("COALESCE(sa.auto_debit_facility, sp.auto_debit_facility) as autoDebitFacility, "); - sqlBuilder.append("sp.loan_facility as loanFacility, sp.premature_withdrawal_option as prematureWithdrawalOption, "); - sqlBuilder.append("sp.tenure_flexibility as tenureFlexibility, sp.lien_marked as lienMarked, "); - sqlBuilder.append("sp.maturity_instructions as maturityInstructions, sp.credit_maturity_proceeds as creditMaturityProceeds, "); - sqlBuilder.append("sp.penalty_grace_period_missed_installments as penaltyGracePeriodMissedInstallments, "); - sqlBuilder.append("sp.panel_request_applicable as panelRequestApplicable, "); - sqlBuilder.append("sp.no_penalty_nominee_heir_closure as noPenaltyNomineeHeirClosure, "); - sqlBuilder.append("sp.deceased_settlement as deceasedSettlement, sp.allow_debit_txn_after_due_date as allowDebitTxnAfterDueDate "); - - sqlBuilder.append(this.selectTablesSql()); - sqlBuilder.append("left join m_deposit_account_recurring_detail dard on sa.id = dard.savings_account_id "); - - this.schemaSql = sqlBuilder.toString(); - } - - @Override - public String schema() { - return this.schemaSql; - } - - @Override - public RecurringDepositAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { - - final DepositAccountData depositAccountData = super.mapRow(rs); - - final boolean preClosurePenalApplicable = rs.getBoolean(PRE_CLOSURE_PENAL_APPLICABLE); - final BigDecimal preClosurePenalInterest = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, PRE_CLOSURE_PENAL_INTEREST); - final Integer preClosurePenalInterestOnTypeId = JdbcSupport.getInteger(rs, PRE_CLOSURE_PENAL_INTEREST_ON_ID); - final EnumOptionData preClosurePenalInterestOnType = (preClosurePenalInterestOnTypeId == null) ? null - : SavingsEnumerations.preClosurePenaltyInterestOnType(preClosurePenalInterestOnTypeId); - final Integer minDepositTerm = JdbcSupport.getInteger(rs, MIN_DEPOSIT_TERM); - final Integer maxDepositTerm = JdbcSupport.getInteger(rs, MAX_DEPOSIT_TERM); - final Integer minDepositTermTypeId = JdbcSupport.getInteger(rs, MIN_DEPOSIT_TERM_TYPE_ID); - final EnumOptionData minDepositTermType = (minDepositTermTypeId == null) ? null - : SavingsEnumerations.depositTermFrequencyType(minDepositTermTypeId); - final Integer maxDepositTermTypeId = JdbcSupport.getInteger(rs, MAX_DEPOSIT_TERM_TYPE_ID); - final EnumOptionData maxDepositTermType = (maxDepositTermTypeId == null) ? null - : SavingsEnumerations.depositTermFrequencyType(maxDepositTermTypeId); - final Integer inMultiplesOfDepositTerm = JdbcSupport.getInteger(rs, IN_MULTIPLES_OF_DEPOSIT_TERM); - final Integer inMultiplesOfDepositTermTypeId = JdbcSupport.getInteger(rs, IN_MULTIPLES_OF_DEPOSIT_TERM_TYPE_ID); - final EnumOptionData inMultiplesOfDepositTermType = (inMultiplesOfDepositTermTypeId == null) ? null - : SavingsEnumerations.depositTermFrequencyType(inMultiplesOfDepositTermTypeId); - - final BigDecimal depositAmount = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, DEPOSIT_AMOUNT); - final BigDecimal maturityAmount = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, MATURITY_AMOUNT); - final LocalDate maturityDate = JdbcSupport.getLocalDate(rs, MATURITY_DATE); - final Integer depositPeriod = JdbcSupport.getInteger(rs, DEPOSIT_PERIOD); - final Integer depositPeriodFrequencyTypeId = JdbcSupport.getInteger(rs, DEPOSIT_PERIOD_FREQUENCY_TYPE_ID); - final EnumOptionData depositPeriodFrequencyType = (depositPeriodFrequencyTypeId == null) ? null - : SavingsEnumerations.depositPeriodFrequency(depositPeriodFrequencyTypeId); - final BigDecimal mandatoryRecommendedDepositAmount = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, - MANDATORY_RECOMMENDED_DEPOSIT_AMOUNT); - final BigDecimal totalOverdueAmount = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, TOTAL_OVERDUE_AMOUNT); - final Integer noOfOverdueInstallments = JdbcSupport.getInteger(rs, NO_OF_OVERDUE_INSTALLMENTS); - final boolean isMandatoryDeposit = rs.getBoolean(IS_MANDATORY_DEPOSIT); - final boolean allowWithdrawal = rs.getBoolean(ALLOW_WITHDRAWAL); - final boolean adjustAdvanceTowardsFuturePayments = rs.getBoolean(ADJUST_ADVANCE_TOWARDS_FUTURE_PAYMENTS); - final boolean isCalendarInherited = rs.getBoolean(IS_CALENDAR_INHERITED); - - final Integer onAccountClosureId = JdbcSupport.getInteger(rs, ON_ACCOUNT_CLOSURE_ID); - final EnumOptionData onAccountClosureType = (onAccountClosureId == null) ? null - : SavingsEnumerations.depositAccountOnClosureType(onAccountClosureId); - final LocalDate expectedFirstDepositOnDate = JdbcSupport.getLocalDate(rs, EXPECTED_FIRST_DEPOSIT_ON_DATE); - - final RecurringDepositAccountData result = RecurringDepositAccountData.instance(depositAccountData, - preClosurePenalApplicable, preClosurePenalInterest, preClosurePenalInterestOnType, minDepositTerm, maxDepositTerm, - minDepositTermType, maxDepositTermType, inMultiplesOfDepositTerm, inMultiplesOfDepositTermType, depositAmount, - maturityAmount, maturityDate, depositPeriod, depositPeriodFrequencyType, mandatoryRecommendedDepositAmount, - onAccountClosureType, expectedFirstDepositOnDate, totalOverdueAmount, noOfOverdueInstallments, isMandatoryDeposit, - allowWithdrawal, adjustAdvanceTowardsFuturePayments, isCalendarInherited); - - result.setCbsProductFields(rs.getString("accountType"), rs.getString("customerSegment"), rs.getString("productCode"), - JdbcSupport.getLocalDate(rs, "effectiveDate"), JdbcSupport.getLocalDate(rs, "effectiveTo"), - JdbcSupport.getLocalDate(rs, "endDate"), rs.getBoolean("introducerRequired"), rs.getBoolean("shareLinkingEnabled"), - rs.getBoolean("nominationFacility"), rs.getBoolean("dormancyAlert"), rs.getBoolean("isDormancyTrackingActive"), - JdbcSupport.getInteger(rs, "daysToInactive"), JdbcSupport.getInteger(rs, "daysToDormancy"), - JdbcSupport.getInteger(rs, "daysToEscheat"), JdbcSupport.getInteger(rs, "interestCalculateSelector"), - rs.getString("interestCalculationTypeName"), rs.getString("averagePeriod"), rs.getBoolean("autoPost"), - JdbcSupport.getInteger(rs, "frequencyFixDay"), JdbcSupport.getLocalDate(rs, "lastInterestDate"), - rs.getBoolean("enableLockinPeriod"), rs.getString("balanceFrequency"), - rs.getBoolean("accountClosedZeroBalanceInDayClosing"), rs.getBoolean("balanceSwapOption"), rs.getBoolean("msme"), - rs.getBoolean("autoDebitFacility"), rs.getBoolean("loanFacility"), rs.getBoolean("prematureWithdrawalOption"), - rs.getBoolean("tenureFlexibility"), rs.getBoolean("lienMarked"), rs.getBoolean("maturityInstructions"), - rs.getBoolean("creditMaturityProceeds"), rs.getBoolean("penaltyGracePeriodMissedInstallments"), - rs.getBoolean("panelRequestApplicable"), rs.getBoolean("noPenaltyNomineeHeirClosure"), - rs.getBoolean("deceasedSettlement"), rs.getBoolean("allowDebitTxnAfterDueDate")); - - return result; - - } - } - - private static final class DepositAccountLookupMapper implements RowMapper { - - public static final String ID = "id"; - public static final String ACCOUNT_NUMBER = "accountNumber"; - public static final String DEPOSIT_TYPE_ID = "depositTypeId"; - - public String schema() { - return " sa.id as id, sa.account_no as accountNumber, sa.deposit_type_enum as depositTypeId from m_savings_account sa "; - } - - @Override - public DepositAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { - - final Long id = rs.getLong(ID); - final String name = rs.getString(ACCOUNT_NUMBER); - final Integer depositTypeId = JdbcSupport.getInteger(rs, DEPOSIT_TYPE_ID); - final EnumOptionData depositType = (depositTypeId == null) ? null : SavingsEnumerations.depositType(depositTypeId); - - return DepositAccountData.lookup(id, name, depositType); - } - } - - private static final class SavingsAccountTransactionsMapper implements RowMapper { - - public static final String TRANSACTION_ID = "transactionId"; - public static final String TRANSACTION_TYPE = "transactionType"; - public static final String TRANSACTION_DATE = "transactionDate"; - public static final String TRANSACTION_AMOUNT = "transactionAmount"; - public static final String RUNNING_BALANCE = "runningBalance"; - public static final String REVERSED = "reversed"; - public static final String SAVINGS_ID = "savingsId"; - public static final String ACCOUNT_NO = "accountNo"; - public static final String PAYMENT_TYPE = "paymentType"; - public static final String PAYMENT_TYPE_NAME = "paymentTypeName"; - public static final String ACCOUNT_NUMBER = "accountNumber"; - public static final String CHECK_NUMBER = "checkNumber"; - public static final String ROUTING_CODE = "routingCode"; - public static final String RECEIPT_NUMBER = "receiptNumber"; - public static final String BANK_NUMBER = "bankNumber"; - public static final String CURRENCY_CODE = "currencyCode"; - public static final String CURRENCY_NAME = "currencyName"; - public static final String CURRENCY_NAME_CODE = "currencyNameCode"; - public static final String CURRENCY_DISPLAY_SYMBOL = "currencyDisplaySymbol"; - public static final String CURRENCY_DIGITS = "currencyDigits"; - public static final String IN_MULTIPLES_OF = "inMultiplesOf"; - public static final String FROM_TRANSFER_ID = "fromTransferId"; - public static final String TO_TRANSFER_ID = "toTransferId"; - public static final String FROM_TRANSFER_DATE = "fromTransferDate"; - public static final String FROM_TRANSFER_AMOUNT = "fromTransferAmount"; - public static final String FROM_TRANSFER_REVERSED = "fromTransferReversed"; - public static final String FROM_TRANSFER_DESCRIPTION = "fromTransferDescription"; - public static final String TO_TRANSFER_DATE = "toTransferDate"; - public static final String TO_TRANSFER_AMOUNT = "toTransferAmount"; - public static final String TO_TRANSFER_REVERSED = "toTransferReversed"; - public static final String TO_TRANSFER_DESCRIPTION = "toTransferDescription"; - public static final String SUBMITTED_BY_USERNAME = "submittedByUsername"; - public static final String SUBMITTED_ON_DATE = "submittedOnDate"; - private final String schemaSql; - - SavingsAccountTransactionsMapper() { - - final StringBuilder sqlBuilder = new StringBuilder(400); - sqlBuilder.append("tr.id as transactionId, tr.transaction_type_enum as transactionType, "); - sqlBuilder.append("tr.transaction_date as transactionDate, tr.amount as transactionAmount,"); - sqlBuilder.append("tr.running_balance_derived as runningBalance, tr.is_reversed as reversed,"); - sqlBuilder.append("tr.submitted_on_date as submittedOnDate,"); - sqlBuilder.append("fromtran.id as fromTransferId, fromtran.is_reversed as fromTransferReversed,"); - sqlBuilder.append("fromtran.transaction_date as fromTransferDate, fromtran.amount as fromTransferAmount,"); - sqlBuilder.append("fromtran.description as fromTransferDescription,"); - sqlBuilder.append("totran.id as toTransferId, totran.is_reversed as toTransferReversed,"); - sqlBuilder.append("totran.transaction_date as toTransferDate, totran.amount as toTransferAmount,"); - sqlBuilder.append("totran.description as toTransferDescription,"); - sqlBuilder.append("sa.id as savingsId, sa.account_no as accountNo,"); - sqlBuilder.append(" au.username as submittedByUsername, "); - sqlBuilder.append("pd.payment_type_id as paymentType,pd.account_number as accountNumber,pd.check_number as checkNumber, "); - sqlBuilder.append("pd.receipt_number as receiptNumber, pd.bank_number as bankNumber,pd.routing_code as routingCode, "); - sqlBuilder.append( - "sa.currency_code as currencyCode, sa.currency_digits as currencyDigits, 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("pt.value as paymentTypeName "); - sqlBuilder.append("from m_savings_account sa "); - sqlBuilder.append("join m_savings_account_transaction tr on tr.savings_account_id = sa.id "); - sqlBuilder.append("join m_currency curr on curr.code = sa.currency_code "); - sqlBuilder.append("left join m_account_transfer_transaction fromtran on fromtran.from_savings_transaction_id = tr.id "); - sqlBuilder.append("left join m_account_transfer_transaction totran on totran.to_savings_transaction_id = tr.id "); - sqlBuilder.append("left join m_payment_detail pd on tr.payment_detail_id = pd.id "); - sqlBuilder.append("left join m_payment_type pt on pd.payment_type_id = pt.id "); - sqlBuilder.append("left join m_appuser au on au.id = tr." + CREATED_BY_DB_FIELD); - this.schemaSql = sqlBuilder.toString(); - } - - public String schema() { - return this.schemaSql; - } - - @Override - public SavingsAccountTransactionData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { - final Long id = rs.getLong(TRANSACTION_ID); - final int transactionTypeInt = JdbcSupport.getInteger(rs, TRANSACTION_TYPE); - final SavingsAccountTransactionEnumData transactionType = SavingsEnumerations.transactionType(transactionTypeInt); - - final LocalDate date = JdbcSupport.getLocalDate(rs, TRANSACTION_DATE); - final LocalDate submittedOnDate = JdbcSupport.getLocalDate(rs, SUBMITTED_ON_DATE); - final BigDecimal amount = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, TRANSACTION_AMOUNT); - final BigDecimal outstandingChargeAmount = null; - final BigDecimal runningBalance = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, RUNNING_BALANCE); - final boolean reversed = rs.getBoolean(REVERSED); - - final Long savingsId = rs.getLong(SAVINGS_ID); - final String accountNo = rs.getString(ACCOUNT_NO); - - PaymentDetailData paymentDetailData = null; - if (transactionType.isDepositOrWithdrawal()) { - final Long paymentTypeId = JdbcSupport.getLong(rs, PAYMENT_TYPE); - if (paymentTypeId != null) { - final String typeName = rs.getString(PAYMENT_TYPE_NAME); - final PaymentTypeData paymentType = PaymentTypeData.builder().id(paymentTypeId).name(typeName).build(); - final String accountNumber = rs.getString(ACCOUNT_NUMBER); - final String checkNumber = rs.getString(CHECK_NUMBER); - final String routingCode = rs.getString(ROUTING_CODE); - final String receiptNumber = rs.getString(RECEIPT_NUMBER); - final String bankNumber = rs.getString(BANK_NUMBER); - paymentDetailData = new PaymentDetailData(id, paymentType, accountNumber, checkNumber, routingCode, receiptNumber, - bankNumber); - } - } - - final String currencyCode = rs.getString(CURRENCY_CODE); - final String currencyName = rs.getString(CURRENCY_NAME); - final String currencyNameCode = rs.getString(CURRENCY_NAME_CODE); - final String currencyDisplaySymbol = rs.getString(CURRENCY_DISPLAY_SYMBOL); - final Integer currencyDigits = JdbcSupport.getInteger(rs, CURRENCY_DIGITS); - final Integer inMultiplesOf = JdbcSupport.getInteger(rs, IN_MULTIPLES_OF); - final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMultiplesOf, currencyDisplaySymbol, - currencyNameCode); - - AccountTransferData transfer = null; - final Long fromTransferId = JdbcSupport.getLong(rs, FROM_TRANSFER_ID); - final Long toTransferId = JdbcSupport.getLong(rs, TO_TRANSFER_ID); - if (fromTransferId != null) { - final LocalDate fromTransferDate = JdbcSupport.getLocalDate(rs, FROM_TRANSFER_DATE); - final BigDecimal fromTransferAmount = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, FROM_TRANSFER_AMOUNT); - final boolean fromTransferReversed = rs.getBoolean(FROM_TRANSFER_REVERSED); - final String fromTransferDescription = rs.getString(FROM_TRANSFER_DESCRIPTION); - - transfer = AccountTransferData.transferBasicDetails(fromTransferId, currency, fromTransferAmount, fromTransferDate, - fromTransferDescription, fromTransferReversed); - } else if (toTransferId != null) { - final LocalDate toTransferDate = JdbcSupport.getLocalDate(rs, TO_TRANSFER_DATE); - final BigDecimal toTransferAmount = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, TO_TRANSFER_AMOUNT); - final boolean toTransferReversed = rs.getBoolean(TO_TRANSFER_REVERSED); - final String toTransferDescription = rs.getString(TO_TRANSFER_DESCRIPTION); - - transfer = AccountTransferData.transferBasicDetails(toTransferId, currency, toTransferAmount, toTransferDate, - toTransferDescription, toTransferReversed); - } - final boolean postInterestAsOn = false; - final String submittedByUsername = rs.getString(SUBMITTED_BY_USERNAME); - final String note = null; - return SavingsAccountTransactionData.create(id, transactionType, paymentDetailData, savingsId, accountNo, date, currency, - amount, outstandingChargeAmount, runningBalance, reversed, transfer, postInterestAsOn, submittedByUsername, note, - submittedOnDate); - } - } - - private abstract static class DepositAccountTemplateMapper implements RowMapper { - - public static final String DEPOSIT_TYPE_ID = "depositTypeId"; - public static final String PRODUCT_ID = "productId"; - public static final String PRODUCT_NAME = "productName"; - public static final String CURRENCY_CODE = "currencyCode"; - public static final String CURRENCY_NAME = "currencyName"; - public static final String CURRENCY_NAME_CODE = "currencyNameCode"; - public static final String CURRENCY_DISPLAY_SYMBOL = "currencyDisplaySymbol"; - public static final String CURRENCY_DIGITS = "currencyDigits"; - public static final String IN_MULTIPLES_OF = "inMultiplesOf"; - public static final String NOMINAL_ANNUAL_ITEREST_RATE = "nominalAnnualIterestRate"; - public static final String INTEREST_COMPOUNDING_PERIOD_TYPE = "interestCompoundingPeriodType"; - public static final String INTEREST_POSTING_PERIOD_TYPE = "interestPostingPeriodType"; - public static final String INTEREST_CALCULATION_TYPE = "interestCalculationType"; - public static final String INTEREST_CALCULATION_DAYS_IN_YEAR_TYPE = "interestCalculationDaysInYearType"; - public static final String MIN_REQUIRED_OPENING_BALANCE = "minRequiredOpeningBalance"; - public static final String LOCKIN_PERIOD_FREQUENCY = "lockinPeriodFrequency"; - public static final String LOCKIN_PERIOD_FREQUENCY_TYPE = "lockinPeriodFrequencyType"; - public static final String WITHDRAWAL_FEE_FOR_TRANSFERS = "withdrawalFeeForTransfers"; - public static final String MIN_BALANCE_FOR_INTEREST_CALCULATION = "minBalanceForInterestCalculation"; - public static final String WITH_HOLD_TAX = "withHoldTax"; - public static final String TAX_GROUP_ID = "taxGroupId"; - public static final String TAX_GROUP_NAME = "taxGroupName"; - private final String selectFieldsSql; - private final String selectTablesSql; - - private final ClientData client; - private final GroupGeneralData group; - - protected DepositAccountTemplateMapper(final ClientData client, final GroupGeneralData group) { - this.client = client; - this.group = group; - - final StringBuilder selectFieldsSqlBuilder = new StringBuilder(400); - selectFieldsSqlBuilder.append("sa.id as productId, sa.name as productName, "); - selectFieldsSqlBuilder.append( - "sa.currency_code as currencyCode, sa.currency_digits as currencyDigits, sa.currency_multiplesof as inMultiplesOf, "); - selectFieldsSqlBuilder.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, "); - selectFieldsSqlBuilder.append("curr.display_symbol as currencyDisplaySymbol, "); - selectFieldsSqlBuilder.append("sa.nominal_annual_interest_rate as nominalAnnualIterestRate, "); - selectFieldsSqlBuilder.append("sa.interest_compounding_period_enum as interestCompoundingPeriodType, "); - selectFieldsSqlBuilder.append("sa.interest_posting_period_enum as interestPostingPeriodType, "); - selectFieldsSqlBuilder.append("sa.interest_calculation_type_enum as interestCalculationType, "); - selectFieldsSqlBuilder.append("sa.interest_calculation_days_in_year_type_enum as interestCalculationDaysInYearType, "); - selectFieldsSqlBuilder.append("sa.min_required_opening_balance as minRequiredOpeningBalance, "); - selectFieldsSqlBuilder.append("sa.lockin_period_frequency as lockinPeriodFrequency,"); - selectFieldsSqlBuilder.append("sa.lockin_period_frequency_enum as lockinPeriodFrequencyType, "); - selectFieldsSqlBuilder.append("sa.withdrawal_fee_for_transfer as withdrawalFeeForTransfers, "); - selectFieldsSqlBuilder.append("sa.deposit_type_enum as depositTypeId, "); - selectFieldsSqlBuilder.append("sa.min_balance_for_interest_calculation as minBalanceForInterestCalculation, "); - selectFieldsSqlBuilder.append("sa.withhold_tax as withHoldTax,"); - selectFieldsSqlBuilder.append("tg.id as taxGroupId, tg.name as taxGroupName "); - - this.selectFieldsSql = selectFieldsSqlBuilder.toString(); - - final StringBuilder selectTablesSqlBuilder = new StringBuilder(400); - selectTablesSqlBuilder.append("from m_savings_product sa "); - selectTablesSqlBuilder.append("left join m_deposit_product_term_and_preclosure dptp on sa.id = dptp.savings_product_id "); - selectTablesSqlBuilder.append("join m_currency curr on curr.code = sa.currency_code "); - selectTablesSqlBuilder.append("left join m_tax_group tg on tg.id = sa.tax_group_id "); - - this.selectTablesSql = selectTablesSqlBuilder.toString(); - } - - public String selectFieldsSql() { - return this.selectFieldsSql; - } - - public String selectTablesSql() { - return this.selectTablesSql; - } - - public abstract String schema(); - - public DepositAccountData mapRow(final ResultSet rs) throws SQLException { - - final Integer depositTypeId = JdbcSupport.getInteger(rs, DEPOSIT_TYPE_ID); - final EnumOptionData depositType = (depositTypeId == null) ? null : SavingsEnumerations.depositType(depositTypeId); - final Long productId = rs.getLong(PRODUCT_ID); - final String productName = rs.getString(PRODUCT_NAME); - - final String currencyCode = rs.getString(CURRENCY_CODE); - final String currencyName = rs.getString(CURRENCY_NAME); - final String currencyNameCode = rs.getString(CURRENCY_NAME_CODE); - final String currencyDisplaySymbol = rs.getString(CURRENCY_DISPLAY_SYMBOL); - final Integer currencyDigits = JdbcSupport.getInteger(rs, CURRENCY_DIGITS); - final Integer inMultiplesOf = JdbcSupport.getInteger(rs, IN_MULTIPLES_OF); - final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMultiplesOf, currencyDisplaySymbol, - currencyNameCode); - - final BigDecimal nominalAnnualIterestRate = rs.getBigDecimal(NOMINAL_ANNUAL_ITEREST_RATE); - - final EnumOptionData interestCompoundingPeriodType = SavingsEnumerations.compoundingInterestPeriodType( - SavingsCompoundingInterestPeriodType.fromInt(JdbcSupport.getInteger(rs, INTEREST_COMPOUNDING_PERIOD_TYPE))); - - final EnumOptionData interestPostingPeriodType = SavingsEnumerations.interestPostingPeriodType( - SavingsPostingInterestPeriodType.fromInt(JdbcSupport.getInteger(rs, INTEREST_POSTING_PERIOD_TYPE))); - - final EnumOptionData interestCalculationType = SavingsEnumerations - .interestCalculationType(SavingsInterestCalculationType.fromInt(JdbcSupport.getInteger(rs, INTEREST_CALCULATION_TYPE))); - - final EnumOptionData interestCalculationDaysInYearType = SavingsEnumerations.interestCalculationDaysInYearType( - SavingsInterestCalculationDaysInYearType.fromInt(JdbcSupport.getInteger(rs, INTEREST_CALCULATION_DAYS_IN_YEAR_TYPE))); - - final BigDecimal minRequiredOpeningBalance = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, MIN_REQUIRED_OPENING_BALANCE); - - final Integer lockinPeriodFrequency = JdbcSupport.getInteger(rs, LOCKIN_PERIOD_FREQUENCY); - EnumOptionData lockinPeriodFrequencyType = null; - final Integer lockinPeriodFrequencyTypeValue = JdbcSupport.getInteger(rs, LOCKIN_PERIOD_FREQUENCY_TYPE); - if (lockinPeriodFrequencyTypeValue != null) { - final SavingsPeriodFrequencyType lockinPeriodType = SavingsPeriodFrequencyType.fromInt(lockinPeriodFrequencyTypeValue); - lockinPeriodFrequencyType = SavingsEnumerations.lockinPeriodFrequencyType(lockinPeriodType); - } - - final boolean withdrawalFeeForTransfers = rs.getBoolean(WITHDRAWAL_FEE_FOR_TRANSFERS); - final BigDecimal minBalanceForInterestCalculation = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, - MIN_BALANCE_FOR_INTEREST_CALCULATION); - - Long clientId = null; - String clientName = null; - if (this.client != null) { - clientId = this.client.getId(); - clientName = this.client.getDisplayName(); - } - - Long groupId = null; - String groupName = null; - if (this.group != null) { - groupId = this.group.getId(); - groupName = this.group.getName(); - } - - final Long fieldOfficerId = null; - final String fieldOfficerName = null; - final SavingsAccountStatusEnumData status = null; - final SavingsAccountSummaryData summary = null; - final SavingsAccountApplicationTimelineData timeline = SavingsAccountApplicationTimelineData.templateDefault(); - - final boolean withHoldTax = rs.getBoolean(WITH_HOLD_TAX); - final Long taxGroupId = JdbcSupport.getLong(rs, TAX_GROUP_ID); - final String taxGroupName = rs.getString(TAX_GROUP_NAME); - TaxGroupData taxGroupData = null; - if (taxGroupId != null) { - taxGroupData = TaxGroupData.lookup(taxGroupId, taxGroupName); - } - - return DepositAccountData.instance(null, null, null, groupId, groupName, clientId, clientName, productId, productName, - fieldOfficerId, fieldOfficerName, status, timeline, currency, nominalAnnualIterestRate, interestCompoundingPeriodType, - interestPostingPeriodType, interestCalculationType, interestCalculationDaysInYearType, minRequiredOpeningBalance, - lockinPeriodFrequency, lockinPeriodFrequencyType, withdrawalFeeForTransfers, summary, depositType, - minBalanceForInterestCalculation, withHoldTax, taxGroupData); - } - } - - private static class FixedDepositAccountTemplateMapper extends DepositAccountTemplateMapper { - - public static final String PRE_CLOSURE_PENAL_APPLICABLE = "preClosurePenalApplicable"; - public static final String PRE_CLOSURE_PENAL_INTEREST = "preClosurePenalInterest"; - public static final String PRE_CLOSURE_PENAL_INTEREST_ON_ID = "preClosurePenalInterestOnId"; - public static final String MIN_DEPOSIT_TERM = "minDepositTerm"; - public static final String MAX_DEPOSIT_TERM = "maxDepositTerm"; - public static final String MIN_DEPOSIT_TERM_TYPE_ID = "minDepositTermTypeId"; - public static final String MAX_DEPOSIT_TERM_TYPE_ID = "maxDepositTermTypeId"; - public static final String IN_MULTIPLES_OF_DEPOSIT_TERM = "inMultiplesOfDepositTerm"; - public static final String IN_MULTIPLES_OF_DEPOSIT_TERM_TYPE_ID = "inMultiplesOfDepositTermTypeId"; - private final String schemaSql; - - FixedDepositAccountTemplateMapper(final ClientData client, final GroupGeneralData group) { - super(client, group); - final StringBuilder sqlBuilder = new StringBuilder(400); - sqlBuilder.append(super.selectFieldsSql()); - - sqlBuilder.append(", dptp.pre_closure_penal_applicable as preClosurePenalApplicable, "); - sqlBuilder.append("dptp.pre_closure_penal_interest as preClosurePenalInterest, "); - sqlBuilder.append("dptp.pre_closure_penal_interest_on_enum as preClosurePenalInterestOnId, "); - sqlBuilder.append("sa.account_type as accountType, sa.customer_segment as customerSegment, sa.fd_type as fdType, "); - sqlBuilder.append("sa.product_code as productCode, "); - sqlBuilder.append("sa.effective_date as effectiveDate, sa.effective_to as effectiveTo, sa.end_date as endDate, "); - sqlBuilder.append("sa.introducer_required as introducerRequired, sa.share_linking_enabled as shareLinkingEnabled, "); - sqlBuilder.append("sa.nomination_facility as nominationFacility, sa.dormancy_alert as dormancyAlert, "); - sqlBuilder.append("sa.is_dormancy_tracking_active as isDormancyTrackingActive, "); - sqlBuilder.append("sa.days_to_inactive as daysToInactive, sa.days_to_dormancy as daysToDormancy, "); - sqlBuilder.append("sa.days_to_escheat as daysToEscheat, "); - sqlBuilder.append("sa.interest_calculate_selector as interestCalculateSelector, "); - sqlBuilder.append("icv.code_value as interestCalculationTypeName, "); - sqlBuilder.append("sa.average_period as averagePeriod, "); - sqlBuilder.append("sa.auto_post as autoPost, sa.frequency_fix_day as frequencyFixDay, "); - sqlBuilder.append("sa.last_interest_date as lastInterestDate, sa.enable_lockin_period as enableLockinPeriod, "); - sqlBuilder.append("sa.balance_frequency as balanceFrequency, "); - sqlBuilder.append("sa.account_closed_zero_balance_in_day_closing as accountClosedZeroBalanceInDayClosing, "); - sqlBuilder.append("sa.balance_swap_option as balanceSwapOption, sa.msme as msme, "); - sqlBuilder.append("sa.auto_debit_facility as autoDebitFacility, sa.loan_facility as loanFacility, "); - sqlBuilder.append("sa.premature_withdrawal_option as prematureWithdrawalOption, sa.tenure_flexibility as tenureFlexibility, "); - sqlBuilder.append("sa.lien_marked as lienMarked, sa.maturity_instructions as maturityInstructions, "); - sqlBuilder.append("sa.credit_maturity_proceeds as creditMaturityProceeds, "); - sqlBuilder.append("sa.penalty_grace_period_missed_installments as penaltyGracePeriodMissedInstallments, "); - sqlBuilder.append("sa.panel_request_applicable as panelRequestApplicable, "); - sqlBuilder.append("sa.no_penalty_nominee_heir_closure as noPenaltyNomineeHeirClosure, "); - sqlBuilder.append("sa.deceased_settlement as deceasedSettlement, "); - sqlBuilder.append("sa.allow_debit_txn_after_due_date as allowDebitTxnAfterDueDate, "); - sqlBuilder.append("dptp.min_deposit_term as minDepositTerm, "); - sqlBuilder.append("dptp.max_deposit_term as maxDepositTerm, "); - sqlBuilder.append("dptp.min_deposit_term_type_enum as minDepositTermTypeId, "); - sqlBuilder.append("dptp.max_deposit_term_type_enum as maxDepositTermTypeId, "); - sqlBuilder.append("dptp.in_multiples_of_deposit_term as inMultiplesOfDepositTerm, "); - sqlBuilder.append("dptp.in_multiples_of_deposit_term_type_enum as inMultiplesOfDepositTermTypeId "); - - sqlBuilder.append(super.selectTablesSql()); - sqlBuilder.append("left join m_code_value icv on icv.id = sa.interest_calculate_selector "); - - this.schemaSql = sqlBuilder.toString(); - } - - @Override - public String schema() { - return this.schemaSql; - } - - @Override - public FixedDepositAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { - - final DepositAccountData depositAccountData = super.mapRow(rs); - - final boolean preClosurePenalApplicable = rs.getBoolean(PRE_CLOSURE_PENAL_APPLICABLE); - final BigDecimal preClosurePenalInterest = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, PRE_CLOSURE_PENAL_INTEREST); - final Integer preClosurePenalInterestOnTypeId = JdbcSupport.getInteger(rs, PRE_CLOSURE_PENAL_INTEREST_ON_ID); - final EnumOptionData preClosurePenalInterestOnType = (preClosurePenalInterestOnTypeId == null) ? null - : SavingsEnumerations.preClosurePenaltyInterestOnType(preClosurePenalInterestOnTypeId); - - final Integer minDepositTerm = JdbcSupport.getInteger(rs, MIN_DEPOSIT_TERM); - final Integer maxDepositTerm = JdbcSupport.getInteger(rs, MAX_DEPOSIT_TERM); - final Integer minDepositTermTypeId = JdbcSupport.getInteger(rs, MIN_DEPOSIT_TERM_TYPE_ID); - final EnumOptionData minDepositTermType = (minDepositTermTypeId == null) ? null - : SavingsEnumerations.depositTermFrequencyType(minDepositTermTypeId); - final Integer maxDepositTermTypeId = JdbcSupport.getInteger(rs, MAX_DEPOSIT_TERM_TYPE_ID); - final EnumOptionData maxDepositTermType = (maxDepositTermTypeId == null) ? null - : SavingsEnumerations.depositTermFrequencyType(maxDepositTermTypeId); - final Integer inMultiplesOfDepositTerm = JdbcSupport.getInteger(rs, IN_MULTIPLES_OF_DEPOSIT_TERM); - final Integer inMultiplesOfDepositTermTypeId = JdbcSupport.getInteger(rs, IN_MULTIPLES_OF_DEPOSIT_TERM_TYPE_ID); - final EnumOptionData inMultiplesOfDepositTermType = (inMultiplesOfDepositTermTypeId == null) ? null - : SavingsEnumerations.depositTermFrequencyType(inMultiplesOfDepositTermTypeId); - - final BigDecimal depositAmount = null; - final BigDecimal maturityAmount = null; - final LocalDate maturityDate = null; - final Integer depositPeriod = null; - final EnumOptionData depositPeriodFrequencyType = null; - final EnumOptionData onAccountClosureType = null; - final Boolean transferInterestToSavings = false; - - final FixedDepositAccountData result = FixedDepositAccountData.instance(depositAccountData, preClosurePenalApplicable, - preClosurePenalInterest, preClosurePenalInterestOnType, minDepositTerm, maxDepositTerm, minDepositTermType, - maxDepositTermType, inMultiplesOfDepositTerm, inMultiplesOfDepositTermType, depositAmount, maturityAmount, - maturityDate, depositPeriod, depositPeriodFrequencyType, onAccountClosureType, transferInterestToSavings, null); - result.setCbsProductFields(rs.getString("accountType"), rs.getString("customerSegment"), rs.getString("fdType"), rs.getString("productCode"), - JdbcSupport.getLocalDate(rs, "effectiveDate"), JdbcSupport.getLocalDate(rs, "effectiveTo"), - JdbcSupport.getLocalDate(rs, "endDate"), rs.getBoolean("introducerRequired"), rs.getBoolean("shareLinkingEnabled"), - rs.getBoolean("nominationFacility"), rs.getBoolean("dormancyAlert"), rs.getBoolean("isDormancyTrackingActive"), - JdbcSupport.getLong(rs, "daysToInactive"), JdbcSupport.getLong(rs, "daysToDormancy"), - JdbcSupport.getLong(rs, "daysToEscheat"), JdbcSupport.getInteger(rs, "interestCalculateSelector"), - rs.getString("interestCalculationTypeName"), rs.getString("averagePeriod"), rs.getBoolean("autoPost"), - JdbcSupport.getInteger(rs, "frequencyFixDay"), JdbcSupport.getLocalDate(rs, "lastInterestDate"), - rs.getBoolean("enableLockinPeriod"), rs.getString("balanceFrequency"), - rs.getBoolean("accountClosedZeroBalanceInDayClosing"), rs.getBoolean("balanceSwapOption"), rs.getBoolean("msme"), - rs.getBoolean("autoDebitFacility"), rs.getBoolean("loanFacility"), rs.getBoolean("prematureWithdrawalOption"), - rs.getBoolean("tenureFlexibility"), rs.getBoolean("lienMarked"), rs.getBoolean("maturityInstructions"), - rs.getBoolean("creditMaturityProceeds"), rs.getBoolean("penaltyGracePeriodMissedInstallments"), - rs.getBoolean("panelRequestApplicable"), rs.getBoolean("noPenaltyNomineeHeirClosure"), - rs.getBoolean("deceasedSettlement"), rs.getBoolean("allowDebitTxnAfterDueDate")); - return result; - } - } - - private static class RecurringDepositAccountTemplateMapper extends DepositAccountTemplateMapper { - - public static final String PRE_CLOSURE_PENAL_APPLICABLE = "preClosurePenalApplicable"; - public static final String PRE_CLOSURE_PENAL_INTEREST = "preClosurePenalInterest"; - public static final String PRE_CLOSURE_PENAL_INTEREST_ON_ID = "preClosurePenalInterestOnId"; - public static final String MIN_DEPOSIT_TERM = "minDepositTerm"; - public static final String MAX_DEPOSIT_TERM = "maxDepositTerm"; - public static final String MIN_DEPOSIT_TERM_TYPE_ID = "minDepositTermTypeId"; - public static final String MAX_DEPOSIT_TERM_TYPE_ID = "maxDepositTermTypeId"; - public static final String IN_MULTIPLES_OF_DEPOSIT_TERM = "inMultiplesOfDepositTerm"; - public static final String IN_MULTIPLES_OF_DEPOSIT_TERM_TYPE_ID = "inMultiplesOfDepositTermTypeId"; - public static final String IS_MANDATORY_DEPOSIT = "isMandatoryDeposit"; - public static final String ALLOW_WITHDRAWAL = "allowWithdrawal"; - public static final String ADJUST_ADVANCE_TOWARDS_FUTURE_PAYMENTS = "adjustAdvanceTowardsFuturePayments"; - private final String schemaSql; - - RecurringDepositAccountTemplateMapper(final ClientData client, final GroupGeneralData group) { - super(client, group); - final StringBuilder sqlBuilder = new StringBuilder(400); - sqlBuilder.append(this.selectFieldsSql()); - - sqlBuilder.append(", dptp.pre_closure_penal_applicable as preClosurePenalApplicable, "); - sqlBuilder.append("dptp.pre_closure_penal_interest as preClosurePenalInterest, "); - sqlBuilder.append("dptp.pre_closure_penal_interest_on_enum as preClosurePenalInterestOnId, "); - sqlBuilder.append("dprd.is_mandatory as isMandatoryDeposit, "); - sqlBuilder.append("dprd.allow_withdrawal as allowWithdrawal, "); - sqlBuilder.append("dprd.adjust_advance_towards_future_payments as adjustAdvanceTowardsFuturePayments, "); - sqlBuilder.append("dptp.min_deposit_term as minDepositTerm, "); - sqlBuilder.append("dptp.max_deposit_term as maxDepositTerm, "); - sqlBuilder.append("dptp.min_deposit_term_type_enum as minDepositTermTypeId, "); - sqlBuilder.append("dptp.max_deposit_term_type_enum as maxDepositTermTypeId, "); - sqlBuilder.append("dptp.in_multiples_of_deposit_term as inMultiplesOfDepositTerm, "); - sqlBuilder.append("dptp.in_multiples_of_deposit_term_type_enum as inMultiplesOfDepositTermTypeId, "); - sqlBuilder.append("sa.account_type as accountType, sa.customer_segment as customerSegment, "); - sqlBuilder.append("sa.product_code as productCode, "); - sqlBuilder.append("sa.effective_date as effectiveDate, sa.effective_to as effectiveTo, sa.end_date as endDate, "); - sqlBuilder.append("sa.introducer_required as introducerRequired, sa.share_linking_enabled as shareLinkingEnabled, "); - sqlBuilder.append("sa.nomination_facility as nominationFacility, sa.dormancy_alert as dormancyAlert, "); - sqlBuilder.append("sa.is_dormancy_tracking_active as isDormancyTrackingActive, "); - sqlBuilder.append("sa.days_to_inactive as daysToInactive, "); - sqlBuilder.append("sa.days_to_dormancy as daysToDormancy, "); - sqlBuilder.append("sa.days_to_escheat as daysToEscheat, "); - sqlBuilder.append("sa.interest_calculate_selector as interestCalculateSelector, "); - sqlBuilder.append("icv.code_value as interestCalculationTypeName, "); - sqlBuilder.append("sa.average_period as averagePeriod, "); - sqlBuilder.append("sa.auto_post as autoPost, sa.frequency_fix_day as frequencyFixDay, "); - sqlBuilder.append("sa.last_interest_date as lastInterestDate, sa.enable_lockin_period as enableLockinPeriod, "); - sqlBuilder.append("sa.balance_frequency as balanceFrequency, "); - sqlBuilder.append("sa.account_closed_zero_balance_in_day_closing as accountClosedZeroBalanceInDayClosing, "); - sqlBuilder.append("sa.balance_swap_option as balanceSwapOption, sa.msme as msme, "); - sqlBuilder.append("sa.auto_debit_facility as autoDebitFacility, sa.loan_facility as loanFacility, "); - sqlBuilder.append("sa.premature_withdrawal_option as prematureWithdrawalOption, sa.tenure_flexibility as tenureFlexibility, "); - sqlBuilder.append("sa.lien_marked as lienMarked, sa.maturity_instructions as maturityInstructions, "); - sqlBuilder.append("sa.credit_maturity_proceeds as creditMaturityProceeds, "); - sqlBuilder.append("sa.penalty_grace_period_missed_installments as penaltyGracePeriodMissedInstallments, "); - sqlBuilder.append("sa.panel_request_applicable as panelRequestApplicable, "); - sqlBuilder.append("sa.no_penalty_nominee_heir_closure as noPenaltyNomineeHeirClosure, "); - sqlBuilder.append("sa.deceased_settlement as deceasedSettlement, "); - sqlBuilder.append("sa.allow_debit_txn_after_due_date as allowDebitTxnAfterDueDate "); - - sqlBuilder.append(this.selectTablesSql()); - sqlBuilder.append("left join m_deposit_product_recurring_detail dprd on sa.id = dprd.savings_product_id "); - sqlBuilder.append("left join m_code_value icv on icv.id = sa.interest_calculate_selector "); - - this.schemaSql = sqlBuilder.toString(); - } - - @Override - public String schema() { - return this.schemaSql; - } - - @Override - public RecurringDepositAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { - - final DepositAccountData depositAccountData = super.mapRow(rs); - - final boolean preClosurePenalApplicable = rs.getBoolean(PRE_CLOSURE_PENAL_APPLICABLE); - final BigDecimal preClosurePenalInterest = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, PRE_CLOSURE_PENAL_INTEREST); - final Integer preClosurePenalInterestOnTypeId = JdbcSupport.getInteger(rs, PRE_CLOSURE_PENAL_INTEREST_ON_ID); - final EnumOptionData preClosurePenalInterestOnType = (preClosurePenalInterestOnTypeId == null) ? null - : SavingsEnumerations.preClosurePenaltyInterestOnType(preClosurePenalInterestOnTypeId); - - final Integer minDepositTerm = JdbcSupport.getInteger(rs, MIN_DEPOSIT_TERM); - final Integer maxDepositTerm = JdbcSupport.getInteger(rs, MAX_DEPOSIT_TERM); - final Integer minDepositTermTypeId = JdbcSupport.getInteger(rs, MIN_DEPOSIT_TERM_TYPE_ID); - final EnumOptionData minDepositTermType = (minDepositTermTypeId == null) ? null - : SavingsEnumerations.depositTermFrequencyType(minDepositTermTypeId); - final Integer maxDepositTermTypeId = JdbcSupport.getInteger(rs, MAX_DEPOSIT_TERM_TYPE_ID); - final EnumOptionData maxDepositTermType = (maxDepositTermTypeId == null) ? null - : SavingsEnumerations.depositTermFrequencyType(maxDepositTermTypeId); - final Integer inMultiplesOfDepositTerm = JdbcSupport.getInteger(rs, IN_MULTIPLES_OF_DEPOSIT_TERM); - final Integer inMultiplesOfDepositTermTypeId = JdbcSupport.getInteger(rs, IN_MULTIPLES_OF_DEPOSIT_TERM_TYPE_ID); - final EnumOptionData inMultiplesOfDepositTermType = (inMultiplesOfDepositTermTypeId == null) ? null - : SavingsEnumerations.depositTermFrequencyType(inMultiplesOfDepositTermTypeId); - final boolean isMandatoryDeposit = rs.getBoolean(IS_MANDATORY_DEPOSIT); - final boolean allowWithdrawal = rs.getBoolean(ALLOW_WITHDRAWAL); - final boolean adjustAdvanceTowardsFuturePayments = rs.getBoolean(ADJUST_ADVANCE_TOWARDS_FUTURE_PAYMENTS); - final boolean isCalendarInherited = false; - - final BigDecimal depositAmount = null; - final BigDecimal maturityAmount = null; - final LocalDate maturityDate = null; - final Integer depositPeriod = null; - final EnumOptionData depositPeriodFrequencyType = null; - final LocalDate expectedFirstDepositOnDate = null; - final BigDecimal mandatoryRecommendedDepositAmount = null; - final EnumOptionData onAccountClosureType = null; - final BigDecimal totalOverdueAmount = null; - final Integer noOfOverdueInstallments = null; - - final RecurringDepositAccountData result = RecurringDepositAccountData.instance(depositAccountData, - preClosurePenalApplicable, preClosurePenalInterest, - preClosurePenalInterestOnType, minDepositTerm, maxDepositTerm, minDepositTermType, maxDepositTermType, - inMultiplesOfDepositTerm, inMultiplesOfDepositTermType, depositAmount, maturityAmount, maturityDate, depositPeriod, - depositPeriodFrequencyType, mandatoryRecommendedDepositAmount, onAccountClosureType, expectedFirstDepositOnDate, - totalOverdueAmount, noOfOverdueInstallments, isMandatoryDeposit, allowWithdrawal, adjustAdvanceTowardsFuturePayments, - isCalendarInherited); - - // CBS product fields - result.setCbsProductFields( - rs.getString("accountType"), - rs.getString("customerSegment"), - rs.getString("productCode"), - JdbcSupport.getLocalDate(rs, "effectiveDate"), - JdbcSupport.getLocalDate(rs, "effectiveTo"), - JdbcSupport.getLocalDate(rs, "endDate"), - rs.getBoolean("introducerRequired"), - rs.getBoolean("shareLinkingEnabled"), - rs.getBoolean("nominationFacility"), - rs.getBoolean("dormancyAlert"), - rs.getBoolean("isDormancyTrackingActive"), - JdbcSupport.getInteger(rs, "daysToInactive"), - JdbcSupport.getInteger(rs, "daysToDormancy"), - JdbcSupport.getInteger(rs, "daysToEscheat"), - JdbcSupport.getInteger(rs, "interestCalculateSelector"), - rs.getString("interestCalculationTypeName"), - rs.getString("averagePeriod"), - rs.getBoolean("autoPost"), - JdbcSupport.getInteger(rs, "frequencyFixDay"), - JdbcSupport.getLocalDate(rs, "lastInterestDate"), - rs.getBoolean("enableLockinPeriod"), - rs.getString("balanceFrequency"), - rs.getBoolean("accountClosedZeroBalanceInDayClosing"), - rs.getBoolean("balanceSwapOption"), - rs.getBoolean("msme"), - rs.getBoolean("autoDebitFacility"), - rs.getBoolean("loanFacility"), - rs.getBoolean("prematureWithdrawalOption"), - rs.getBoolean("tenureFlexibility"), - rs.getBoolean("lienMarked"), - rs.getBoolean("maturityInstructions"), - rs.getBoolean("creditMaturityProceeds"), - rs.getBoolean("penaltyGracePeriodMissedInstallments"), - rs.getBoolean("panelRequestApplicable"), - rs.getBoolean("noPenaltyNomineeHeirClosure"), - rs.getBoolean("deceasedSettlement"), - rs.getBoolean("allowDebitTxnAfterDueDate") - ); - - return result; - } - } - - private static final class DepositAccountForMaturityMapper implements RowMapper { - - public static final String ID = "id"; - public static final String ACCOUNT_NUMBER = "accountNumber"; - public static final String DEPOSIT_TYPE_ID = "depositTypeId"; - - public String schema() { - LocalDate today = DateUtils.getBusinessLocalDate(); - String formattedToday = DATE_TIME_FORMATTER.format(today); - final StringBuilder sqlBuilder = new StringBuilder(200); - sqlBuilder.append("da.id as id, "); - sqlBuilder.append("da.account_no as accountNumber, "); - sqlBuilder.append("da.deposit_type_enum as depositTypeId "); - sqlBuilder.append("FROM m_savings_account da "); - sqlBuilder.append("inner join m_deposit_account_term_and_preclosure dat on dat.savings_account_id = da.id "); - sqlBuilder.append("and dat.maturity_date is not null and dat.maturity_date <= '" + formattedToday + "' "); - - return sqlBuilder.toString(); - } - - @Override - public DepositAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { - - final Long id = rs.getLong(ID); - final String name = rs.getString(ACCOUNT_NUMBER); - final Integer depositTypeId = JdbcSupport.getInteger(rs, DEPOSIT_TYPE_ID); - final EnumOptionData depositType = (depositTypeId == null) ? null : SavingsEnumerations.depositType(depositTypeId); - - return DepositAccountData.lookup(id, name, depositType); - } - } - - private static final class RecurringAccountDepositTransactionTemplateMapper implements RowMapper { - - public static final String ID = "id"; - public static final String ACCOUNT_NO = "accountNo"; - public static final String DUEAMOUNT = "dueamount"; - public static final String OUTSTANDING_CHARGE_AMOUNT = "outstandingChargeAmount"; - public static final String DUEDATE = "duedate"; - public static final String CURRENCY_CODE = "currencyCode"; - public static final String CURRENCY_NAME = "currencyName"; - public static final String CURRENCY_NAME_CODE = "currencyNameCode"; - public static final String CURRENCY_DISPLAY_SYMBOL = "currencyDisplaySymbol"; - public static final String CURRENCY_DIGITS = "currencyDigits"; - public static final String IN_MULTIPLES_OF = "inMultiplesOf"; - public static final String RUNNING_BALANCE = "runningBalance"; - private final String schemaSql; - - RecurringAccountDepositTransactionTemplateMapper() { - final StringBuilder sqlBuilder = new StringBuilder(400); - sqlBuilder.append("sa.id as id, sa.account_no as accountNo, "); - sqlBuilder.append( - "sa.currency_code as currencyCode, sa.currency_digits as currencyDigits, 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("sa.account_balance_derived as runningBalance, "); - sqlBuilder.append( - "mss.duedate as duedate, (mss.deposit_amount - coalesce(mss.deposit_amount_completed_derived,0)) as dueamount, "); - sqlBuilder.append("coalesce(sac.amount_outstanding_derived,0.0) AS outstandingChargeAmount "); - sqlBuilder.append("from m_savings_account sa "); - sqlBuilder.append("join m_mandatory_savings_schedule mss on mss.savings_account_id=sa.id and mss.completed_derived = false "); - sqlBuilder.append("join m_currency curr on curr.code = sa.currency_code "); - sqlBuilder.append("LEFT JOIN(SELECT s.savings_account_id AS savings_account_id "); - sqlBuilder.append(",SUM(COALESCE(s.amount_outstanding_derived,0.0)) AS amount_outstanding_derived "); - sqlBuilder.append("FROM m_savings_account_charge s "); - sqlBuilder.append("JOIN m_charge c ON c.id = s.charge_id AND c.charge_time_enum = 3 "); - sqlBuilder.append("WHERE s.savings_account_id = ? "); - sqlBuilder.append("AND s.is_active = TRUE GROUP BY s.savings_account_id)sac ON sac.savings_account_id = sa.id "); - - this.schemaSql = sqlBuilder.toString(); - } - - public String schema() { - return this.schemaSql; - } - - @Override - public SavingsAccountTransactionData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { - - final Long savingsId = rs.getLong(ID); - final String accountNo = rs.getString(ACCOUNT_NO); - final BigDecimal dueamount = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, DUEAMOUNT); - final BigDecimal outstandingChargeAmount = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, OUTSTANDING_CHARGE_AMOUNT); - final LocalDate duedate = JdbcSupport.getLocalDate(rs, DUEDATE); - final String currencyCode = rs.getString(CURRENCY_CODE); - final String currencyName = rs.getString(CURRENCY_NAME); - final String currencyNameCode = rs.getString(CURRENCY_NAME_CODE); - final String currencyDisplaySymbol = rs.getString(CURRENCY_DISPLAY_SYMBOL); - final Integer currencyDigits = JdbcSupport.getInteger(rs, CURRENCY_DIGITS); - final Integer inMultiplesOf = JdbcSupport.getInteger(rs, IN_MULTIPLES_OF); - final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMultiplesOf, currencyDisplaySymbol, - currencyNameCode); - final SavingsAccountTransactionEnumData transactionType = SavingsEnumerations - .transactionType(SavingsAccountTransactionType.DEPOSIT.getValue()); - final PaymentDetailData paymentDetailData = null; - final AccountTransferData transfer = null; - final BigDecimal runningBalance = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, RUNNING_BALANCE); - final boolean postInterestAsOn = false; - final String submittedByUsername = null; - final String note = null; - final LocalDate submittedOnDate = DateUtils.getBusinessLocalDate(); - return SavingsAccountTransactionData.create(savingsId, transactionType, paymentDetailData, savingsId, accountNo, duedate, - currency, dueamount, outstandingChargeAmount, runningBalance, false, transfer, postInterestAsOn, submittedByUsername, - note, submittedOnDate); - } - } - - private static class AccountTransferMapper implements RowMapper { - - public static final String FROM_ACC = "fromAcc"; - public static final String TO_ACC = "toAcc"; - public static final String AMOUNT = "amount"; - public static final String TRANSACTION_DATE = "transactionDate"; - public static final String TRANSFER_INTEREST_TO_SAVINGS = "transfer interest to savings"; - private final String schemaSql; - - AccountTransferMapper() { - final StringBuilder sqlBuilder = new StringBuilder(400); - sqlBuilder.append( - "sa.id as fromAcc ,aa.linked_savings_account_id as toAcc,st.amount as amount, st.transaction_date as transactionDate ") - .append(" from m_deposit_account_term_and_preclosure da ") - .append(" inner join m_savings_account sa on da.savings_account_id = sa.id") - .append(" inner join m_savings_account_transaction st on st.savings_account_id = sa.id") - .append(" inner join m_portfolio_account_associations aa on aa.savings_account_id=sa.id"); - schemaSql = sqlBuilder.toString(); - } - - public String schema() { - return this.schemaSql; - } - - @Override - public AccountTransferDTO mapRow(ResultSet rs, @SuppressWarnings("unused") int rowNum) throws SQLException { - final Long fromAccountId = rs.getLong(FROM_ACC); - final Long toAccountId = rs.getLong(TO_ACC); - final BigDecimal transactionAmount = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, AMOUNT); - final boolean isRegularTransaction = false; - final boolean isExceptionForBalanceCheck = false; - final LocalDate transactionDate = JdbcSupport.getLocalDate(rs, TRANSACTION_DATE); - return new AccountTransferDTO(transactionDate, transactionAmount, PortfolioAccountType.SAVINGS, PortfolioAccountType.SAVINGS, - fromAccountId, toAccountId, TRANSFER_INTEREST_TO_SAVINGS, null, null, null, null, null, null, null, - AccountTransferType.INTEREST_TRANSFER.getValue(), null, null, ExternalId.empty(), null, null, null, - isRegularTransaction, 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.service; + +import static org.apache.fineract.infrastructure.core.domain.AuditableFieldsConstants.CREATED_BY_DB_FIELD; + +import java.math.BigDecimal; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.infrastructure.codes.data.CodeValueData; +import org.apache.fineract.infrastructure.codes.service.CodeValueReadPlatformService; +import org.apache.fineract.infrastructure.core.data.EnumOptionData; +import org.apache.fineract.infrastructure.core.data.PaginationParameters; +import org.apache.fineract.infrastructure.core.data.PaginationParametersDataValidator; +import org.apache.fineract.infrastructure.core.domain.ExternalId; +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.database.DatabaseSpecificSQLGenerator; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.organisation.monetary.data.CurrencyData; +import org.apache.fineract.organisation.staff.data.StaffData; +import org.apache.fineract.organisation.staff.service.StaffReadPlatformService; +import org.apache.fineract.portfolio.account.PortfolioAccountType; +import org.apache.fineract.portfolio.account.service.AccountNumberGenerator; +import org.apache.fineract.portfolio.account.data.AccountTransferDTO; +import org.apache.fineract.portfolio.account.data.AccountTransferData; +import org.apache.fineract.portfolio.account.domain.AccountTransferType; +import org.apache.fineract.portfolio.calendar.data.CalendarData; +import org.apache.fineract.portfolio.calendar.domain.CalendarEntityType; +import org.apache.fineract.portfolio.calendar.domain.CalendarFrequencyType; +import org.apache.fineract.portfolio.calendar.domain.CalendarType; +import org.apache.fineract.portfolio.calendar.service.CalendarReadPlatformService; +import org.apache.fineract.portfolio.charge.data.ChargeData; +import org.apache.fineract.portfolio.charge.service.ChargeReadPlatformService; +import org.apache.fineract.portfolio.charge.util.ConvertChargeDataToSpecificChargeData; +import org.apache.fineract.portfolio.client.data.ClientData; +import org.apache.fineract.portfolio.client.service.ClientReadPlatformService; +import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; +import org.apache.fineract.portfolio.common.service.CommonEnumerations; +import org.apache.fineract.portfolio.common.service.DropdownReadPlatformService; +import org.apache.fineract.portfolio.group.data.GroupGeneralData; +import org.apache.fineract.portfolio.group.service.GroupReadPlatformService; +import org.apache.fineract.portfolio.interestratechart.data.InterestRateChartData; +import org.apache.fineract.portfolio.interestratechart.service.InterestRateChartReadPlatformService; +import org.apache.fineract.portfolio.paymentdetail.data.PaymentDetailData; +import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData; +import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeReadService; +import org.apache.fineract.portfolio.savings.SavingsApiConstants; +import org.apache.fineract.portfolio.savings.DepositAccountOnClosureType; +import org.apache.fineract.portfolio.savings.DepositAccountType; +import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType; +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.DepositAccountData; +import org.apache.fineract.portfolio.savings.data.DepositAccountInterestRateChartData; +import org.apache.fineract.portfolio.savings.data.DepositProductData; +import org.apache.fineract.portfolio.savings.data.FixedDepositAccountData; +import org.apache.fineract.portfolio.savings.data.RecurringDepositAccountData; +import org.apache.fineract.portfolio.savings.data.SavingsAccountApplicationTimelineData; +import org.apache.fineract.portfolio.savings.data.SavingsAccountChargeData; +import org.apache.fineract.portfolio.savings.data.SavingsAccountData; +import org.apache.fineract.portfolio.savings.data.SavingsAccountStatusEnumData; +import org.apache.fineract.portfolio.savings.data.SavingsAccountSummaryData; +import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData; +import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionEnumData; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountStatusType; +import org.apache.fineract.portfolio.savings.exception.DepositAccountNotFoundException; +import org.apache.fineract.portfolio.tax.data.TaxGroupData; +import org.apache.fineract.useradministration.domain.AppUser; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.CollectionUtils; + +@Slf4j +@RequiredArgsConstructor +public class DepositAccountReadPlatformServiceImpl implements DepositAccountReadPlatformService { + + private static final FixedDepositAccountMapper FIXED_DEPOSIT_ACCOUNT_MAPPER = new FixedDepositAccountMapper(); + private static final RecurringDepositAccountMapper RECURRING_DEPOSIT_ACCOUNT_MAPPER = new RecurringDepositAccountMapper(); + private static final DepositAccountLookupMapper DEPOSIT_ACCOUNT_LOOKUP_MAPPER = new DepositAccountLookupMapper(); + private static final DepositAccountForMaturityMapper DEPOSIT_ACCOUNT_FOR_MATURITY_MAPPER = new DepositAccountForMaturityMapper(); + // allowed column names for sorting the query result + private static final Set supportedOrderByValues = new HashSet<>(Arrays.asList("id", "accountNumbr", "officeId", "officeName")); + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + private final PlatformSecurityContext context; + private final JdbcTemplate jdbcTemplate; + private final DepositAccountInterestRateChartReadPlatformService accountChartReadPlatformService; + private final InterestRateChartReadPlatformService productChartReadPlatformService; + private final PaginationParametersDataValidator paginationParametersDataValidator; + private final DatabaseSpecificSQLGenerator sqlGenerator; + private final PaginationHelper paginationHelper; + private final SavingsAccountTransactionsMapper transactionsMapper = new SavingsAccountTransactionsMapper(); + private final ClientReadPlatformService clientReadPlatformService; + private final GroupReadPlatformService groupReadPlatformService; + private final DepositProductReadPlatformService depositProductReadPlatformService; + private final SavingsDropdownReadPlatformService savingsDropdownReadPlatformService; + private final ChargeReadPlatformService chargeReadPlatformService; + private final StaffReadPlatformService staffReadPlatformService; + private final DepositsDropdownReadPlatformService depositsDropdownReadPlatformService; + private final SavingsAccountReadPlatformService savingsAccountReadPlatformService; + private final RecurringAccountDepositTransactionTemplateMapper rdTransactionTemplateMapper = new RecurringAccountDepositTransactionTemplateMapper(); + private final DropdownReadPlatformService dropdownReadPlatformService; + private final CalendarReadPlatformService calendarReadPlatformService; + private final PaymentTypeReadService paymentTypeReadPlatformService; + private final AccountNumberGenerator accountNumberGenerator; + private final CodeValueReadPlatformService codeValueReadPlatformService; + + @Override + public Collection retrieveAll(final DepositAccountType depositAccountType, + final PaginationParameters paginationParameters) { + + this.context.authenticatedUser(); + this.paginationParametersDataValidator.validateParameterValues(paginationParameters, supportedOrderByValues, + depositAccountType.resourceName()); + + final DepositAccountMapper depositAccountMapper = this.getDepositAccountMapper(depositAccountType); + if (depositAccountMapper == null) { + return null; + } + + final StringBuilder sqlBuilder = new StringBuilder(400); + sqlBuilder.append("select "); + sqlBuilder.append(depositAccountMapper.schema()); + sqlBuilder.append(" where sa.deposit_type_enum = ? "); + // always append at the end of a sql statement + sqlBuilder.append(paginationParameters.paginationSql()); + + return jdbcTemplate.query(sqlBuilder.toString(), depositAccountMapper, new Object[] { depositAccountType.getValue() }); // NOSONAR + } + + @Override + public Page retrieveAllPaged(final DepositAccountType depositAccountType, + final PaginationParameters paginationParameters) { + + this.paginationParametersDataValidator.validateParameterValues(paginationParameters, supportedOrderByValues, + depositAccountType.resourceName()); + + final DepositAccountMapper depositAccountMapper = this.getDepositAccountMapper(depositAccountType); + if (depositAccountMapper == null) { + return null; + } + + final StringBuilder sqlBuilder = new StringBuilder(400); + sqlBuilder.append("select " + sqlGenerator.calcFoundRows() + " "); + sqlBuilder.append(depositAccountMapper.schema()); + sqlBuilder.append(" where sa.deposit_type_enum = ? "); + sqlBuilder.append(paginationParameters.paginationSql()); + + return this.paginationHelper.fetchPage(this.jdbcTemplate, sqlBuilder.toString(), new Object[] { depositAccountType.getValue() }, + depositAccountMapper); + } + + @Override + public Page retrieveAllPaged(final DepositAccountType depositAccountType, + final PaginationParameters paginationParameters, final String accountNo, final String clientName) { + + this.paginationParametersDataValidator.validateParameterValues(paginationParameters, supportedOrderByValues, + depositAccountType.resourceName()); + + final DepositAccountMapper depositAccountMapper = this.getDepositAccountMapper(depositAccountType); + if (depositAccountMapper == null) { + return null; + } + + final StringBuilder sqlBuilder = new StringBuilder(400); + sqlBuilder.append("select " + sqlGenerator.calcFoundRows() + " "); + sqlBuilder.append(depositAccountMapper.schema()); + sqlBuilder.append(" where sa.deposit_type_enum = ? "); + + final List params = new ArrayList<>(); + params.add(depositAccountType.getValue()); + if (StringUtils.isNotBlank(accountNo)) { + sqlBuilder.append(" and sa.account_no like ? "); + params.add("%" + accountNo + "%"); + } + if (StringUtils.isNotBlank(clientName)) { + sqlBuilder.append(" and c.display_name like ? "); + params.add("%" + clientName + "%"); + } + sqlBuilder.append(paginationParameters.paginationSql()); + + return this.paginationHelper.fetchPage(this.jdbcTemplate, sqlBuilder.toString(), params.toArray(), depositAccountMapper); + } + + @Override + public Collection retrieveAllForLookup(final DepositAccountType depositAccountType) { + + final StringBuilder sqlBuilder = new StringBuilder(400); + sqlBuilder.append("select "); + sqlBuilder.append(DEPOSIT_ACCOUNT_LOOKUP_MAPPER.schema()); + sqlBuilder.append(" where sa.deposit_type_enum = ? "); + + return this.jdbcTemplate.query(sqlBuilder.toString(), DEPOSIT_ACCOUNT_LOOKUP_MAPPER, depositAccountType.getValue()); + } + + @Override + public Collection retrieveForMaturityUpdate() { + + final StringBuilder sqlBuilder = new StringBuilder(200); + sqlBuilder.append("SELECT "); + sqlBuilder.append(DEPOSIT_ACCOUNT_FOR_MATURITY_MAPPER.schema()); + sqlBuilder.append(" WHERE da.deposit_type_enum in (?, ?) and da.status_enum = ?"); + + return this.jdbcTemplate.query(sqlBuilder.toString(), DEPOSIT_ACCOUNT_FOR_MATURITY_MAPPER, + DepositAccountType.FIXED_DEPOSIT.getValue(), DepositAccountType.RECURRING_DEPOSIT.getValue(), + SavingsAccountStatusType.ACTIVE.getValue()); + } + + @Override + public DepositAccountData retrieveOne(final DepositAccountType depositAccountType, final Long accountId) { + try { + this.context.authenticatedUser(); + + final DepositAccountMapper depositAccountMapper = this.getDepositAccountMapper(depositAccountType); + if (depositAccountMapper == null) { + return null; + } + + final StringBuilder sqlBuilder = new StringBuilder(400); + sqlBuilder.append("select "); + sqlBuilder.append(depositAccountMapper.schema()); + sqlBuilder.append(" where sa.id = ? and sa.deposit_type_enum = ? "); + + return this.jdbcTemplate.queryForObject(sqlBuilder.toString(), depositAccountMapper, accountId, depositAccountType.getValue()); + + } catch (final EmptyResultDataAccessException e) { + throw new DepositAccountNotFoundException(depositAccountType, accountId, e); + } + } + + @Override + public DepositAccountData retrieveOneWithClosureTemplate(final DepositAccountType depositAccountType, final Long accountId) { + try { + this.context.authenticatedUser(); + + final DepositAccountMapper depositAccountMapper = this.getDepositAccountMapper(depositAccountType); + if (depositAccountMapper == null) { + return null; + } + + final StringBuilder sqlBuilder = new StringBuilder(400); + sqlBuilder.append("select "); + sqlBuilder.append(depositAccountMapper.schema()); + sqlBuilder.append(" where sa.id = ? and sa.deposit_type_enum = ? "); + + DepositAccountData account = this.jdbcTemplate.queryForObject(sqlBuilder.toString(), depositAccountMapper, accountId, + depositAccountType.getValue()); + Collection onAccountClosureOptions = SavingsEnumerations + .depositAccountOnClosureType(DepositAccountOnClosureType.values()); + final Collection paymentTypeOptions = this.paymentTypeReadPlatformService.retrieveAllPaymentTypes(); + final Collection savingsAccountDatas = this.savingsAccountReadPlatformService + .retrieveActiveForLookup(account.getClientId(), DepositAccountType.SAVINGS_DEPOSIT); + if (depositAccountType.isFixedDeposit()) { + account = FixedDepositAccountData.withClosureTemplateDetails((FixedDepositAccountData) account, onAccountClosureOptions, + paymentTypeOptions, savingsAccountDatas); + } else if (depositAccountType.isRecurringDeposit()) { + account = RecurringDepositAccountData.withClosureTemplateDetails((RecurringDepositAccountData) account, + onAccountClosureOptions, paymentTypeOptions, savingsAccountDatas); + } + + return account; + + } catch (final EmptyResultDataAccessException e) { + throw new DepositAccountNotFoundException(depositAccountType, accountId, e); + } + } + + @Override + public DepositAccountData retrieveOneWithChartSlabs(final DepositAccountType depositAccountType, Long accountId) { + DepositAccountData depositAccount = this.retrieveOne(depositAccountType, accountId); + DepositAccountInterestRateChartData chart = this.accountChartReadPlatformService.retrieveOneWithSlabsOnAccountId(accountId); + + if (depositAccountType.isFixedDeposit()) { + depositAccount = FixedDepositAccountData.withInterestChart((FixedDepositAccountData) depositAccount, chart); + } else if (depositAccountType.isRecurringDeposit()) { + CalendarData calendar = this.calendarReadPlatformService.retrieveCollctionCalendarByEntity(accountId, + CalendarEntityType.SAVINGS.getValue()); + Integer frequency = 1; + EnumOptionData frequencyType = null; + if (calendar != null) { + frequency = calendar.getInterval() == -1 ? 1 : calendar.getInterval(); + final CalendarFrequencyType calendarFrequencyType = CalendarFrequencyType + .fromInt(calendar.getFrequency().getId().intValue()); + final PeriodFrequencyType periodFrequencyType = CalendarFrequencyType.from(calendarFrequencyType); + frequencyType = CommonEnumerations.termFrequencyType(periodFrequencyType, "recurring.deposit.frequency."); + } + depositAccount = RecurringDepositAccountData.withInterestChartAndRecurringDetails((RecurringDepositAccountData) depositAccount, + chart, frequency, frequencyType); + } + + return depositAccount; + } + + @Override + public Collection retrieveAllTransactions(final DepositAccountType depositAccountType, + final Long accountId) { + + final String sql = "select " + this.transactionsMapper.schema() + + " where sa.id = ? and sa.deposit_type_enum = ? order by tr.transaction_date DESC, tr.id DESC"; + + return this.jdbcTemplate.query(sql, this.transactionsMapper, new Object[] { accountId, depositAccountType.getValue() }); // NOSONAR + } + + @Override + public DepositAccountData retrieveTemplate(final DepositAccountType depositAccountType, final Long clientId, final Long groupId, + final Long productId, final boolean staffInSelectedOfficeOnly) { + + final AppUser loggedInUser = this.context.authenticatedUser(); + Long officeId = loggedInUser.getOffice().getId(); + + ClientData client = null; + Collection savingsAccountDatas = null; + if (clientId != null) { + client = this.clientReadPlatformService.retrieveOne(clientId); + officeId = client.getOfficeId(); + savingsAccountDatas = this.savingsAccountReadPlatformService.retrieveActiveForLookup(clientId, + DepositAccountType.SAVINGS_DEPOSIT); + } + + GroupGeneralData group = null; + if (groupId != null) { + group = this.groupReadPlatformService.retrieveOne(groupId); + officeId = group.getOfficeId(); + } + + final Collection preClosurePenalInterestOnTypeOptions = this.depositsDropdownReadPlatformService + .retrievePreClosurePenalInterestOnTypeOptions(); + + final Collection periodFrequencyTypeOptions = this.dropdownReadPlatformService.retrievePeriodFrequencyTypeOptions(); + final Collection productOptions = this.depositProductReadPlatformService + .retrieveAllForLookup(depositAccountType); + DepositAccountData template = null; + if (productId != null) { + + final DepositAccountTemplateMapper mapper = getDepositAccountTemplaMapper(depositAccountType, client, group); + + final String sql = "select " + mapper.schema() + " where sa.id = ? and sa.deposit_type_enum = ? "; + template = this.jdbcTemplate.queryForObject(sql, mapper, new Object[] { productId, depositAccountType.getValue() }); // NOSONAR + + final Collection interestCompoundingPeriodTypeOptions = this.savingsDropdownReadPlatformService + .retrieveCompoundingInterestPeriodTypeOptions(); + + final Collection interestPostingPeriodTypeOptions = this.savingsDropdownReadPlatformService + .retrieveInterestPostingPeriodTypeOptions(); + + final Collection interestCalculationTypeOptions = this.savingsDropdownReadPlatformService + .retrieveInterestCalculationTypeOptions(); + + final Collection interestCalculationDaysInYearTypeOptions = this.savingsDropdownReadPlatformService + .retrieveInterestCalculationDaysInYearTypeOptions(); + + final Collection lockinPeriodFrequencyTypeOptions = this.savingsDropdownReadPlatformService + .retrieveLockinPeriodFrequencyTypeOptions(); + + final Collection withdrawalFeeTypeOptions = this.savingsDropdownReadPlatformService + .retrievewithdrawalFeeTypeOptions(); + + final Collection transactions = null; + final Collection productCharges = this.chargeReadPlatformService.retrieveSavingsProductCharges(productId); + // update charges from Product charges + final Collection charges = fromChargesToSavingsCharges(productCharges); + + final boolean feeChargesOnly = false; + final Collection chargeOptions = this.chargeReadPlatformService + .retrieveSavingsProductApplicableCharges(feeChargesOnly); + + final Collection maturityInstructionOptions = this.depositsDropdownReadPlatformService + .maturityInstructionOptions(); + + Collection fieldOfficerOptions = null; + + if (officeId != null) { + + if (staffInSelectedOfficeOnly) { + // only bring back loan officers in selected branch/office + final Collection fieldOfficersInBranch = this.staffReadPlatformService + .retrieveAllLoanOfficersInOfficeById(officeId); + + if (!CollectionUtils.isEmpty(fieldOfficersInBranch)) { + fieldOfficerOptions = new ArrayList<>(fieldOfficersInBranch); + } + } else { + // by default bring back all officers in selected + // branch/office as well as officers in office above + // this office + final boolean restrictToLoanOfficersOnly = true; + final Collection loanOfficersInHierarchy = this.staffReadPlatformService + .retrieveAllStaffInOfficeAndItsParentOfficeHierarchy(officeId, restrictToLoanOfficersOnly); + + if (!CollectionUtils.isEmpty(loanOfficersInHierarchy)) { + fieldOfficerOptions = new ArrayList<>(loanOfficersInHierarchy); + } + } + } + + // retrieve chart Slabs + final InterestRateChartData productChartData = this.productChartReadPlatformService.retrieveActiveChartWithTemplate(productId); + final DepositAccountInterestRateChartData accountChart = (productChartData != null) + ? DepositAccountInterestRateChartData.from(productChartData) + : null; + + if (depositAccountType.isFixedDeposit()) { + + template = FixedDepositAccountData.withTemplateOptions((FixedDepositAccountData) template, productOptions, + fieldOfficerOptions, interestCompoundingPeriodTypeOptions, interestPostingPeriodTypeOptions, + interestCalculationTypeOptions, interestCalculationDaysInYearTypeOptions, lockinPeriodFrequencyTypeOptions, + withdrawalFeeTypeOptions, transactions, charges, chargeOptions, preClosurePenalInterestOnTypeOptions, + periodFrequencyTypeOptions, savingsAccountDatas, maturityInstructionOptions); + + template = FixedDepositAccountData.withInterestChart((FixedDepositAccountData) template, accountChart); + + try { + final Collection accountTypeOpts = this.codeValueReadPlatformService + .retrieveCodeValuesByCode(SavingsApiConstants.FD_PRODUCT_ACCOUNT_TYPE); + final Collection customerSegmentOpts = this.codeValueReadPlatformService + .retrieveCodeValuesByCode(SavingsApiConstants.FD_PRODUCT_CUSTOMER_SEGMENT); + ((FixedDepositAccountData) template).setAccountTypeOptions(accountTypeOpts); + ((FixedDepositAccountData) template).setCustomerSegmentOptions(customerSegmentOpts); + } catch (final Exception ex) { + log.warn("Failed to load FD account type/customer segment options: {}", ex.getMessage()); + } + + try { + final String preview = accountNumberGenerator.previewAccountNumber(productId, officeId); + template.setPreviewAccountNo(preview); + } catch (final Exception ex) { + log.warn("Failed to generate preview account number for FD productId={}: {}", productId, ex.getMessage()); + } + } else if (depositAccountType.isRecurringDeposit()) { + // Save CBS fields from mapper result before factory methods create new objects + final RecurringDepositAccountData rdTemplateWithCbs = (RecurringDepositAccountData) template; + template = RecurringDepositAccountData.withTemplateOptions(rdTemplateWithCbs, productOptions, + fieldOfficerOptions, interestCompoundingPeriodTypeOptions, interestPostingPeriodTypeOptions, + interestCalculationTypeOptions, interestCalculationDaysInYearTypeOptions, lockinPeriodFrequencyTypeOptions, + withdrawalFeeTypeOptions, transactions, charges, chargeOptions, preClosurePenalInterestOnTypeOptions, + periodFrequencyTypeOptions); + template = RecurringDepositAccountData.withInterestChartAndRecurringDetails((RecurringDepositAccountData) template, + accountChart, null, null); + // Re-apply CBS product fields lost during factory method object creation + ((RecurringDepositAccountData) template).copyCbsFieldsFrom(rdTemplateWithCbs); + + // Populate account type / customer segment options from code values + try { + final Collection accountTypeOpts = this.codeValueReadPlatformService + .retrieveCodeValuesByCode(SavingsApiConstants.RD_PRODUCT_ACCOUNT_TYPE); + final Collection customerSegmentOpts = this.codeValueReadPlatformService + .retrieveCodeValuesByCode(SavingsApiConstants.RD_PRODUCT_CUSTOMER_SEGMENT); + ((RecurringDepositAccountData) template).setAccountTypeOptions(accountTypeOpts); + ((RecurringDepositAccountData) template).setCustomerSegmentOptions(customerSegmentOpts); + } catch (final Exception ex) { + log.warn("Failed to load RD account type/customer segment options: {}", ex.getMessage()); + } + + // Generate preview account number + try { + final String preview = accountNumberGenerator.previewAccountNumber(productId, officeId); + template.setPreviewAccountNo(preview); + } catch (final Exception ex) { + log.warn("Failed to generate preview account number for RD productId={}: {}", productId, ex.getMessage()); + } + } + + } else { + + String clientName = null; + if (client != null) { + clientName = client.getDisplayName(); + } + + String groupName = null; + if (group != null) { + groupName = group.getName(); + } + + final Collection fieldOfficerOptions = null; + final Collection interestCompoundingPeriodTypeOptions = null; + final Collection interestPostingPeriodTypeOptions = null; + final Collection interestCalculationTypeOptions = null; + final Collection interestCalculationDaysInYearTypeOptions = null; + final Collection lockinPeriodFrequencyTypeOptions = null; + final Collection withdrawalFeeTypeOptions = null; + + final Collection transactions = null; + final Collection charges = null; + final Collection maturityInstructionOptions = null; + + final boolean feeChargesOnly = true; + final Collection chargeOptions = this.chargeReadPlatformService + .retrieveSavingsProductApplicableCharges(feeChargesOnly); + + if (depositAccountType.isFixedDeposit()) { + + template = FixedDepositAccountData.withClientTemplate(clientId, clientName, groupId, groupName); + + template = FixedDepositAccountData.withTemplateOptions((FixedDepositAccountData) template, productOptions, + fieldOfficerOptions, interestCompoundingPeriodTypeOptions, interestPostingPeriodTypeOptions, + interestCalculationTypeOptions, interestCalculationDaysInYearTypeOptions, lockinPeriodFrequencyTypeOptions, + withdrawalFeeTypeOptions, transactions, charges, chargeOptions, preClosurePenalInterestOnTypeOptions, + periodFrequencyTypeOptions, savingsAccountDatas, maturityInstructionOptions); + } else if (depositAccountType.isRecurringDeposit()) { + + template = RecurringDepositAccountData.withClientTemplate(clientId, clientName, groupId, groupName); + + template = RecurringDepositAccountData.withTemplateOptions((RecurringDepositAccountData) template, productOptions, + fieldOfficerOptions, interestCompoundingPeriodTypeOptions, interestPostingPeriodTypeOptions, + interestCalculationTypeOptions, interestCalculationDaysInYearTypeOptions, lockinPeriodFrequencyTypeOptions, + withdrawalFeeTypeOptions, transactions, charges, chargeOptions, preClosurePenalInterestOnTypeOptions, + periodFrequencyTypeOptions); + } + } + + return template; + } + + @Override + public SavingsAccountTransactionData retrieveRecurringAccountDepositTransactionTemplate(final Long accountId) { + + try { + final String sql = "select " + this.rdTransactionTemplateMapper.schema() + + " where sa.id = ? and sa.deposit_type_enum = ? order by mss.installment limit 1"; + return this.jdbcTemplate.queryForObject(sql, this.rdTransactionTemplateMapper, // NOSONAR + accountId, accountId, DepositAccountType.RECURRING_DEPOSIT.getValue()); + } catch (final EmptyResultDataAccessException e) { + throw new DepositAccountNotFoundException(DepositAccountType.RECURRING_DEPOSIT, accountId, e); + } + } + + @Override + public Collection retrieveDataForInterestTransfer() { + final StringBuilder sqlBuilder = new StringBuilder(300); + AccountTransferMapper mapper = new AccountTransferMapper(); + sqlBuilder.append("SELECT "); + sqlBuilder.append(mapper.schema()); + sqlBuilder.append(" where da.transfer_interest_to_linked_account = true and "); + sqlBuilder.append( + "st.transaction_date > (select coalesce(max(sat.transaction_date),sa.activatedon_date) from m_savings_account_transaction sat where sat.transaction_type_enum = ? and sat.savings_account_id = sa.id and sat.is_reversed=false) "); + sqlBuilder.append( + "and st.transaction_type_enum = ? and sa.status_enum = ? and st.is_reversed=false and st.transaction_date > coalesce(sa.lockedin_until_date_derived,sa.activatedon_date)"); + + return this.jdbcTemplate.query(sqlBuilder.toString(), mapper, SavingsAccountTransactionType.WITHDRAWAL.getValue(), + SavingsAccountTransactionType.INTEREST_POSTING.getValue(), SavingsAccountStatusType.ACTIVE.getValue()); + } + + @Override + public Collection> retriveDataForRDScheduleCreation() { + String today = DATE_TIME_FORMATTER.format(DateUtils.getBusinessLocalDate()); + final StringBuilder sb = new StringBuilder(300); + sb.append(" select rd.savings_account_id savingsId, rd.mandatory_recommended_deposit_amount as amount,"); + sb.append(" mc.recurrence as recurrence ,"); + sb.append(" max(ms.duedate) as dueDate , max(ms.installment) as installment,"); + sb.append(" count(ms.installment) as futureInstallments"); + sb.append(" from m_deposit_account_term_and_preclosure dat "); + sb.append(" inner join m_savings_account sa on sa.id = dat.savings_account_id and sa.status_enum = ?"); + sb.append(" inner join m_deposit_account_recurring_detail rd on rd.savings_account_id = dat.savings_account_id "); + sb.append(" inner join m_calendar_instance mci on mci.entity_type_enum = ? and mci.entity_id = dat.savings_account_id "); + sb.append(" inner join m_calendar mc on mc.id = mci.calendar_id and mc.calendar_type_enum = ?"); + sb.append(" inner join m_mandatory_savings_schedule ms on ms.savings_account_id = dat.savings_account_id and ms.duedate > '" + today + + "'"); + sb.append(" where dat.deposit_period is null"); + sb.append(" group by ms.savings_account_id, rd.mandatory_recommended_deposit_amount, mc.recurrence, rd.savings_account_id"); + + return this.jdbcTemplate.queryForList(sb.toString(), SavingsAccountStatusType.ACTIVE.getValue(), + CalendarEntityType.SAVINGS.getValue(), CalendarType.COLLECTION.getValue()); + } + + private DepositAccountMapper getDepositAccountMapper(final DepositAccountType depositAccountType) { + if (depositAccountType.isFixedDeposit()) { + return FIXED_DEPOSIT_ACCOUNT_MAPPER; + } else if (depositAccountType.isRecurringDeposit()) { + return RECURRING_DEPOSIT_ACCOUNT_MAPPER; + } + return null; + } + + private DepositAccountTemplateMapper getDepositAccountTemplaMapper(final DepositAccountType depositAccountType, ClientData client, + GroupGeneralData group) { + if (depositAccountType.isFixedDeposit()) { + return new FixedDepositAccountTemplateMapper(client, group); + } else if (depositAccountType.isRecurringDeposit()) { + return new RecurringDepositAccountTemplateMapper(client, group); + } + return null; + } + + private Collection fromChargesToSavingsCharges(final Collection productCharges) { + final Collection savingsCharges = new ArrayList<>(); + for (final ChargeData chargeData : productCharges) { + final SavingsAccountChargeData savingsCharge = ConvertChargeDataToSpecificChargeData.toSavingsAccountChargeData(chargeData); + savingsCharges.add(savingsCharge); + } + return savingsCharges; + } + + private abstract static class DepositAccountMapper implements RowMapper { + + public static final String ACCOUNT_NO = "accountNo"; + public static final String ID = "id"; + public static final String EXTERNAL_ID = "externalId"; + public static final String GROUP_ID = "groupId"; + public static final String GROUP_NAME = "groupName"; + public static final String CLIENT_ID = "clientId"; + public static final String CLIENT_NAME = "clientName"; + public static final String PRODUCT_ID = "productId"; + public static final String PRODUCT_NAME = "productName"; + public static final String FIELD_OFFICER_ID = "fieldOfficerId"; + public static final String FIELD_OFFICER_NAME = "fieldOfficerName"; + public static final String STATUS_ENUM = "statusEnum"; + public static final String SUBMITTED_ON_DATE = "submittedOnDate"; + public static final String SUBMITTED_BY_USERNAME = "submittedByUsername"; + public static final String SUBMITTED_BY_LASTNAME = "submittedByLastname"; + public static final String SUBMITTED_BY_FIRSTNAME = "submittedByFirstname"; + public static final String REJECTED_ON_DATE = "rejectedOnDate"; + public static final String REJECTED_BY_USERNAME = "rejectedByUsername"; + public static final String REJECTED_BY_FIRSTNAME = "rejectedByFirstname"; + public static final String REJECTED_BY_LASTNAME = "rejectedByLastname"; + public static final String WITHDRAWN_ON_DATE = "withdrawnOnDate"; + public static final String WITHDRAWN_BY_USERNAME = "withdrawnByUsername"; + public static final String WITHDRAWN_BY_FIRSTNAME = "withdrawnByFirstname"; + public static final String WITHDRAWN_BY_LASTNAME = "withdrawnByLastname"; + public static final String APPROVED_ON_DATE = "approvedOnDate"; + public static final String APPROVED_BY_USERNAME = "approvedByUsername"; + public static final String APPROVED_BY_FIRSTNAME = "approvedByFirstname"; + public static final String APPROVED_BY_LASTNAME = "approvedByLastname"; + public static final String ACTIVATED_ON_DATE = "activatedOnDate"; + public static final String ACTIVATED_BY_USERNAME = "activatedByUsername"; + public static final String ACTIVATED_BY_FIRSTNAME = "activatedByFirstname"; + public static final String ACTIVATED_BY_LASTNAME = "activatedByLastname"; + public static final String CLOSED_ON_DATE = "closedOnDate"; + public static final String CLOSED_BY_USERNAME = "closedByUsername"; + public static final String CLOSED_BY_FIRSTNAME = "closedByFirstname"; + public static final String CLOSED_BY_LASTNAME = "closedByLastname"; + public static final String DEPOSIT_TYPE_ID = "depositTypeId"; + public static final String CURRENCY_CODE = "currencyCode"; + public static final String CURRENCY_NAME = "currencyName"; + public static final String CURRENCY_NAME_CODE = "currencyNameCode"; + public static final String CURRENCY_DISPLAY_SYMBOL = "currencyDisplaySymbol"; + public static final String CURRENCY_DIGITS = "currencyDigits"; + public static final String IN_MULTIPLES_OF = "inMultiplesOf"; + public static final String NOMINAL_ANNUAL_INTEREST_RATE = "nominalAnnualInterestRate"; + public static final String INTEREST_COMPOUNDING_PERIOD_TYPE = "interestCompoundingPeriodType"; + public static final String INTEREST_POSTING_PERIOD_TYPE = "interestPostingPeriodType"; + public static final String INTEREST_CALCULATION_TYPE = "interestCalculationType"; + public static final String INTEREST_CALCULATION_DAYS_IN_YEAR_TYPE = "interestCalculationDaysInYearType"; + public static final String MIN_REQUIRED_OPENING_BALANCE = "minRequiredOpeningBalance"; + public static final String LOCKIN_PERIOD_FREQUENCY = "lockinPeriodFrequency"; + public static final String LOCKIN_PERIOD_FREQUENCY_TYPE = "lockinPeriodFrequencyType"; + public static final String WITHDRAWAL_FEE_FOR_TRANSFERS = "withdrawalFeeForTransfers"; + public static final String MIN_BALANCE_FOR_INTEREST_CALCULATION = "minBalanceForInterestCalculation"; + public static final String TOTAL_DEPOSITS = "totalDeposits"; + public static final String TOTAL_WITHDRAWALS = "totalWithdrawals"; + public static final String TOTAL_WITHDRAWAL_FEES = "totalWithdrawalFees"; + public static final String TOTAL_ANNUAL_FEES = "totalAnnualFees"; + public static final String TOTAL_INTEREST_EARNED = "totalInterestEarned"; + public static final String TOTAL_INTEREST_POSTED = "totalInterestPosted"; + public static final String ACCOUNT_BALANCE = "accountBalance"; + public static final String TOTAL_FEE_CHARGE = "totalFeeCharge"; + public static final String TOTAL_PENALTY_CHARGE = "totalPenaltyCharge"; + public static final String TOTAL_WITHHOLD_TAX = "totalWithholdTax"; + public static final String INTEREST_POSTED_TILL_DATE = "interestPostedTillDate"; + public static final String WITH_HOLD_TAX = "withHoldTax"; + public static final String TAX_GROUP_ID = "taxGroupId"; + public static final String TAX_GROUP_NAME = "taxGroupName"; + private final String selectFieldsSql; + private final String selectTablesSql; + + protected DepositAccountMapper() { + final StringBuilder selectFieldsSqlBuilder = new StringBuilder(400); + selectFieldsSqlBuilder.append("sa.id as id, sa.account_no as accountNo, sa.external_id as externalId, "); + selectFieldsSqlBuilder.append("c.id as clientId, c.display_name as clientName, "); + selectFieldsSqlBuilder.append("g.id as groupId, g.display_name as groupName, "); + selectFieldsSqlBuilder.append("sp.id as productId, sp.name as productName, "); + selectFieldsSqlBuilder.append("s.id fieldOfficerId, s.display_name as fieldOfficerName, "); + selectFieldsSqlBuilder.append("sa.status_enum as statusEnum, "); + selectFieldsSqlBuilder.append("sa.submittedon_date as submittedOnDate,"); + selectFieldsSqlBuilder.append("sbu.username as submittedByUsername,"); + selectFieldsSqlBuilder.append("sbu.firstname as submittedByFirstname, sbu.lastname as submittedByLastname,"); + selectFieldsSqlBuilder.append("sa.rejectedon_date as rejectedOnDate,"); + selectFieldsSqlBuilder.append("rbu.username as rejectedByUsername,"); + selectFieldsSqlBuilder.append("rbu.firstname as rejectedByFirstname, rbu.lastname as rejectedByLastname,"); + selectFieldsSqlBuilder.append("sa.withdrawnon_date as withdrawnOnDate,"); + selectFieldsSqlBuilder.append("wbu.username as withdrawnByUsername,"); + selectFieldsSqlBuilder.append("wbu.firstname as withdrawnByFirstname, wbu.lastname as withdrawnByLastname,"); + selectFieldsSqlBuilder.append("sa.approvedon_date as approvedOnDate,"); + selectFieldsSqlBuilder.append("abu.username as approvedByUsername,"); + selectFieldsSqlBuilder.append("abu.firstname as approvedByFirstname, abu.lastname as approvedByLastname,"); + selectFieldsSqlBuilder.append("sa.activatedon_date as activatedOnDate,"); + selectFieldsSqlBuilder.append("avbu.username as activatedByUsername,"); + selectFieldsSqlBuilder.append("avbu.firstname as activatedByFirstname, avbu.lastname as activatedByLastname,"); + selectFieldsSqlBuilder.append("sa.closedon_date as closedOnDate,"); + selectFieldsSqlBuilder.append("cbu.username as closedByUsername,"); + selectFieldsSqlBuilder.append("cbu.firstname as closedByFirstname, cbu.lastname as closedByLastname,"); + selectFieldsSqlBuilder.append( + "sa.currency_code as currencyCode, sa.currency_digits as currencyDigits, sa.currency_multiplesof as inMultiplesOf, "); + selectFieldsSqlBuilder.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, "); + selectFieldsSqlBuilder.append("curr.display_symbol as currencyDisplaySymbol, "); + selectFieldsSqlBuilder.append("sa.nominal_annual_interest_rate as nominalAnnualInterestRate, "); + selectFieldsSqlBuilder.append("sa.interest_compounding_period_enum as interestCompoundingPeriodType, "); + selectFieldsSqlBuilder.append("sa.interest_posting_period_enum as interestPostingPeriodType, "); + selectFieldsSqlBuilder.append("sa.interest_calculation_type_enum as interestCalculationType, "); + selectFieldsSqlBuilder.append("sa.interest_calculation_days_in_year_type_enum as interestCalculationDaysInYearType, "); + selectFieldsSqlBuilder.append("sa.min_required_opening_balance as minRequiredOpeningBalance, "); + selectFieldsSqlBuilder.append("sa.lockin_period_frequency as lockinPeriodFrequency,"); + selectFieldsSqlBuilder.append("sa.lockin_period_frequency_enum as lockinPeriodFrequencyType, "); + selectFieldsSqlBuilder.append("sa.withdrawal_fee_for_transfer as withdrawalFeeForTransfers, "); + selectFieldsSqlBuilder.append("sa.total_deposits_derived as totalDeposits, "); + selectFieldsSqlBuilder.append("sa.total_withdrawals_derived as totalWithdrawals, "); + selectFieldsSqlBuilder.append("sa.interest_posted_till_date as interestPostedTillDate, "); + selectFieldsSqlBuilder.append("sa.total_withdrawal_fees_derived as totalWithdrawalFees, "); + selectFieldsSqlBuilder.append("sa.total_annual_fees_derived as totalAnnualFees, "); + selectFieldsSqlBuilder.append("sa.total_interest_earned_derived as totalInterestEarned, "); + selectFieldsSqlBuilder.append("sa.total_interest_posted_derived as totalInterestPosted, "); + selectFieldsSqlBuilder.append("sa.account_balance_derived as accountBalance, "); + selectFieldsSqlBuilder.append("sa.total_fees_charge_derived as totalFeeCharge, "); + selectFieldsSqlBuilder.append("sa.total_penalty_charge_derived as totalPenaltyCharge, "); + selectFieldsSqlBuilder.append("sa.total_withhold_tax_derived as totalWithholdTax,"); + selectFieldsSqlBuilder.append("sa.deposit_type_enum as depositTypeId, "); + selectFieldsSqlBuilder.append("sa.cbs_account_type as accountType, sa.customer_segment as customerSegment, "); + selectFieldsSqlBuilder.append("sa.account_status as accountStatus, sa.mode_of_operation as modeOfOperation, "); + selectFieldsSqlBuilder.append("sa.special_category as specialCategory, sa.weaker_section as weakerSection, "); + selectFieldsSqlBuilder.append("sa.guardian_name as guardianName, sa.guardian_cif_number as guardianCifNumber, "); + selectFieldsSqlBuilder.append("sa.guardian_relationship as guardianRelationship, sa.guardian_client_id as guardianClientId, "); + selectFieldsSqlBuilder.append("sa.minor_date_of_birth as minorDateOfBirth, "); + selectFieldsSqlBuilder.append("sa.introducer_client_id as introducerClientId, sa.introducer_name as introducerName, "); + selectFieldsSqlBuilder.append("sa.introducer_cif_number as introducerCifNumber, "); + selectFieldsSqlBuilder.append("sa.linked_account_id as linkedAccountId, sa.linked_account_type as linkedAccountType, "); + selectFieldsSqlBuilder.append("sa.linked_account_no as linkedAccountNo, "); + selectFieldsSqlBuilder.append("sa.min_balance_for_interest_calculation as minBalanceForInterestCalculation, "); + selectFieldsSqlBuilder.append("sa.withhold_tax as withHoldTax,"); + selectFieldsSqlBuilder.append("tg.id as taxGroupId, tg.name as taxGroupName "); + + this.selectFieldsSql = selectFieldsSqlBuilder.toString(); + + final StringBuilder selectTablesSqlBuilder = new StringBuilder(400); + selectTablesSqlBuilder.append("from m_savings_account sa "); + selectTablesSqlBuilder.append("left join m_deposit_account_term_and_preclosure datp on sa.id = datp.savings_account_id "); + selectTablesSqlBuilder.append("join m_savings_product sp ON sa.product_id = sp.id "); + selectTablesSqlBuilder.append("left join m_code_value icv on icv.id = sp.interest_calculate_selector "); + selectTablesSqlBuilder.append("join m_currency curr on curr.code = sa.currency_code "); + selectTablesSqlBuilder.append("left join m_client c ON c.id = sa.client_id "); + selectTablesSqlBuilder.append("left join m_group g ON g.id = sa.group_id "); + selectTablesSqlBuilder.append("left join m_staff s ON s.id = sa.field_officer_id "); + selectTablesSqlBuilder.append("left join m_appuser sbu on sbu.id = sa.submittedon_userid "); + selectTablesSqlBuilder.append("left join m_appuser rbu on rbu.id = sa.rejectedon_userid "); + selectTablesSqlBuilder.append("left join m_appuser wbu on wbu.id = sa.withdrawnon_userid "); + selectTablesSqlBuilder.append("left join m_appuser abu on abu.id = sa.approvedon_userid "); + selectTablesSqlBuilder.append("left join m_appuser avbu on avbu.id = sa.activatedon_userid "); + selectTablesSqlBuilder.append("left join m_appuser cbu on cbu.id = sa.closedon_userid "); + selectTablesSqlBuilder.append("left join m_tax_group tg on tg.id = sa.tax_group_id "); + + this.selectTablesSql = selectTablesSqlBuilder.toString(); + } + + public String selectFieldsSql() { + return this.selectFieldsSql; + } + + public String selectTablesSql() { + return this.selectTablesSql; + } + + public abstract String schema(); + + public DepositAccountData mapRow(final ResultSet rs) throws SQLException { + + final Long id = rs.getLong(ID); + final String accountNo = rs.getString(ACCOUNT_NO); + final String externalId = rs.getString(EXTERNAL_ID); + + final Long groupId = JdbcSupport.getLong(rs, GROUP_ID); + final String groupName = rs.getString(GROUP_NAME); + final Long clientId = JdbcSupport.getLong(rs, CLIENT_ID); + final String clientName = rs.getString(CLIENT_NAME); + + final Long productId = rs.getLong(PRODUCT_ID); + final String productName = rs.getString(PRODUCT_NAME); + + final Long fieldOfficerId = rs.getLong(FIELD_OFFICER_ID); + final String fieldOfficerName = rs.getString(FIELD_OFFICER_NAME); + + final Integer statusEnum = JdbcSupport.getInteger(rs, STATUS_ENUM); + final SavingsAccountStatusEnumData status = SavingsEnumerations.status(statusEnum); + + final LocalDate submittedOnDate = JdbcSupport.getLocalDate(rs, SUBMITTED_ON_DATE); + final String submittedByUsername = rs.getString(SUBMITTED_BY_USERNAME); + final String submittedByFirstname = rs.getString(SUBMITTED_BY_FIRSTNAME); + final String submittedByLastname = rs.getString(SUBMITTED_BY_LASTNAME); + + final LocalDate rejectedOnDate = JdbcSupport.getLocalDate(rs, REJECTED_ON_DATE); + final String rejectedByUsername = rs.getString(REJECTED_BY_USERNAME); + final String rejectedByFirstname = rs.getString(REJECTED_BY_FIRSTNAME); + final String rejectedByLastname = rs.getString(REJECTED_BY_LASTNAME); + + final LocalDate withdrawnOnDate = JdbcSupport.getLocalDate(rs, WITHDRAWN_ON_DATE); + final String withdrawnByUsername = rs.getString(WITHDRAWN_BY_USERNAME); + final String withdrawnByFirstname = rs.getString(WITHDRAWN_BY_FIRSTNAME); + final String withdrawnByLastname = rs.getString(WITHDRAWN_BY_LASTNAME); + + final LocalDate approvedOnDate = JdbcSupport.getLocalDate(rs, APPROVED_ON_DATE); + final String approvedByUsername = rs.getString(APPROVED_BY_USERNAME); + final String approvedByFirstname = rs.getString(APPROVED_BY_FIRSTNAME); + final String approvedByLastname = rs.getString(APPROVED_BY_LASTNAME); + + final LocalDate activatedOnDate = JdbcSupport.getLocalDate(rs, ACTIVATED_ON_DATE); + final String activatedByUsername = rs.getString(ACTIVATED_BY_USERNAME); + final String activatedByFirstname = rs.getString(ACTIVATED_BY_FIRSTNAME); + final String activatedByLastname = rs.getString(ACTIVATED_BY_LASTNAME); + + final LocalDate closedOnDate = JdbcSupport.getLocalDate(rs, CLOSED_ON_DATE); + final String closedByUsername = rs.getString(CLOSED_BY_USERNAME); + final String closedByFirstname = rs.getString(CLOSED_BY_FIRSTNAME); + final String closedByLastname = rs.getString(CLOSED_BY_LASTNAME); + + final SavingsAccountApplicationTimelineData timeline = new SavingsAccountApplicationTimelineData(submittedOnDate, + submittedByUsername, submittedByFirstname, submittedByLastname, rejectedOnDate, rejectedByUsername, rejectedByFirstname, + rejectedByLastname, withdrawnOnDate, withdrawnByUsername, withdrawnByFirstname, withdrawnByLastname, approvedOnDate, + approvedByUsername, approvedByFirstname, approvedByLastname, activatedOnDate, activatedByUsername, activatedByFirstname, + activatedByLastname, closedOnDate, closedByUsername, closedByFirstname, closedByLastname); + + final Integer depositTypeId = JdbcSupport.getInteger(rs, DEPOSIT_TYPE_ID); + final EnumOptionData depositType = (depositTypeId == null) ? null : SavingsEnumerations.depositType(depositTypeId); + + final String currencyCode = rs.getString(CURRENCY_CODE); + final String currencyName = rs.getString(CURRENCY_NAME); + final String currencyNameCode = rs.getString(CURRENCY_NAME_CODE); + final String currencyDisplaySymbol = rs.getString(CURRENCY_DISPLAY_SYMBOL); + final Integer currencyDigits = JdbcSupport.getInteger(rs, CURRENCY_DIGITS); + final Integer inMultiplesOf = JdbcSupport.getInteger(rs, IN_MULTIPLES_OF); + final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMultiplesOf, currencyDisplaySymbol, + currencyNameCode); + + final BigDecimal nominalAnnualInterestRate = rs.getBigDecimal(NOMINAL_ANNUAL_INTEREST_RATE); + + final EnumOptionData interestCompoundingPeriodType = SavingsEnumerations.compoundingInterestPeriodType( + SavingsCompoundingInterestPeriodType.fromInt(JdbcSupport.getInteger(rs, INTEREST_COMPOUNDING_PERIOD_TYPE))); + + final EnumOptionData interestPostingPeriodType = SavingsEnumerations.interestPostingPeriodType( + SavingsPostingInterestPeriodType.fromInt(JdbcSupport.getInteger(rs, INTEREST_POSTING_PERIOD_TYPE))); + + final EnumOptionData interestCalculationType = SavingsEnumerations + .interestCalculationType(SavingsInterestCalculationType.fromInt(JdbcSupport.getInteger(rs, INTEREST_CALCULATION_TYPE))); + + final EnumOptionData interestCalculationDaysInYearType = SavingsEnumerations.interestCalculationDaysInYearType( + SavingsInterestCalculationDaysInYearType.fromInt(JdbcSupport.getInteger(rs, INTEREST_CALCULATION_DAYS_IN_YEAR_TYPE))); + + final BigDecimal minRequiredOpeningBalance = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, MIN_REQUIRED_OPENING_BALANCE); + + final Integer lockinPeriodFrequency = JdbcSupport.getInteger(rs, LOCKIN_PERIOD_FREQUENCY); + EnumOptionData lockinPeriodFrequencyType = null; + final Integer lockinPeriodFrequencyTypeValue = JdbcSupport.getInteger(rs, LOCKIN_PERIOD_FREQUENCY_TYPE); + if (lockinPeriodFrequencyTypeValue != null) { + final SavingsPeriodFrequencyType lockinPeriodType = SavingsPeriodFrequencyType.fromInt(lockinPeriodFrequencyTypeValue); + lockinPeriodFrequencyType = SavingsEnumerations.lockinPeriodFrequencyType(lockinPeriodType); + } + + final boolean withdrawalFeeForTransfers = rs.getBoolean(WITHDRAWAL_FEE_FOR_TRANSFERS); + final BigDecimal minBalanceForInterestCalculation = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, + MIN_BALANCE_FOR_INTEREST_CALCULATION); + + final BigDecimal totalDeposits = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, TOTAL_DEPOSITS); + final BigDecimal totalWithdrawals = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, TOTAL_WITHDRAWALS); + final BigDecimal totalWithdrawalFees = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, TOTAL_WITHDRAWAL_FEES); + final BigDecimal totalAnnualFees = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, TOTAL_ANNUAL_FEES); + + final BigDecimal totalInterestEarned = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, TOTAL_INTEREST_EARNED); + final BigDecimal totalInterestPosted = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, TOTAL_INTEREST_POSTED); + final BigDecimal accountBalance = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, ACCOUNT_BALANCE); + final BigDecimal totalFeeCharge = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, TOTAL_FEE_CHARGE); + final BigDecimal totalPenaltyCharge = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, TOTAL_PENALTY_CHARGE); + final BigDecimal totalWithholdTax = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, TOTAL_WITHHOLD_TAX); + final BigDecimal totalOverdraftInterestDerived = null; + final LocalDate interestPostedTillDate = JdbcSupport.getLocalDate(rs, INTEREST_POSTED_TILL_DATE); + + final boolean withHoldTax = rs.getBoolean(WITH_HOLD_TAX); + final Long taxGroupId = JdbcSupport.getLong(rs, TAX_GROUP_ID); + final String taxGroupName = rs.getString(TAX_GROUP_NAME); + TaxGroupData taxGroupData = null; + if (taxGroupId != null) { + taxGroupData = TaxGroupData.lookup(taxGroupId, taxGroupName); + } + + final BigDecimal availableBalance = null; + final SavingsAccountSummaryData summary = new SavingsAccountSummaryData(currency, totalDeposits, totalWithdrawals, + totalWithdrawalFees, totalAnnualFees, totalInterestEarned, totalInterestPosted, accountBalance, totalFeeCharge, + totalPenaltyCharge, totalOverdraftInterestDerived, totalWithholdTax, null, null, availableBalance, + interestPostedTillDate); + + return DepositAccountData.instance(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName, + fieldOfficerId, fieldOfficerName, status, timeline, currency, nominalAnnualInterestRate, interestCompoundingPeriodType, + interestPostingPeriodType, interestCalculationType, interestCalculationDaysInYearType, minRequiredOpeningBalance, + lockinPeriodFrequency, lockinPeriodFrequencyType, withdrawalFeeForTransfers, summary, depositType, + minBalanceForInterestCalculation, withHoldTax, taxGroupData); + } + } + + private static class FixedDepositAccountMapper extends DepositAccountMapper { + + public static final String PRE_CLOSURE_PENAL_APPLICABLE = "preClosurePenalApplicable"; + public static final String PRE_CLOSURE_PENAL_INTEREST = "preClosurePenalInterest"; + public static final String PRE_CLOSURE_PENAL_INTEREST_ON_ID = "preClosurePenalInterestOnId"; + public static final String MIN_DEPOSIT_TERM = "minDepositTerm"; + public static final String MAX_DEPOSIT_TERM = "maxDepositTerm"; + public static final String MIN_DEPOSIT_TERM_TYPE_ID = "minDepositTermTypeId"; + public static final String MAX_DEPOSIT_TERM_TYPE_ID = "maxDepositTermTypeId"; + public static final String IN_MULTIPLES_OF_DEPOSIT_TERM = "inMultiplesOfDepositTerm"; + public static final String IN_MULTIPLES_OF_DEPOSIT_TERM_TYPE_ID = "inMultiplesOfDepositTermTypeId"; + public static final String DEPOSIT_AMOUNT = "depositAmount"; + public static final String MATURITY_AMOUNT = "maturityAmount"; + public static final String MATURITY_DATE = "maturityDate"; + public static final String DEPOSIT_PERIOD = "depositPeriod"; + public static final String DEPOSIT_PERIOD_FREQUENCY_TYPE_ID = "depositPeriodFrequencyTypeId"; + public static final String ON_ACCOUNT_CLOSURE_ID = "onAccountClosureId"; + public static final String TRANSFER_INTEREST_TO_SAVINGS = "transferInterestToSavings"; + public static final String TRANSFER_TO_SAVINGS_ID = "transferToSavingsId"; + private final String schemaSql; + + FixedDepositAccountMapper() { + final StringBuilder sqlBuilder = new StringBuilder(400); + sqlBuilder.append(super.selectFieldsSql()); + + sqlBuilder.append(", datp.pre_closure_penal_applicable as preClosurePenalApplicable, "); + sqlBuilder.append("datp.pre_closure_penal_interest as preClosurePenalInterest, "); + sqlBuilder.append("datp.pre_closure_penal_interest_on_enum as preClosurePenalInterestOnId, "); + sqlBuilder.append("datp.min_deposit_term as minDepositTerm, "); + sqlBuilder.append("datp.max_deposit_term as maxDepositTerm, "); + sqlBuilder.append("datp.min_deposit_term_type_enum as minDepositTermTypeId, "); + sqlBuilder.append("datp.max_deposit_term_type_enum as maxDepositTermTypeId, "); + sqlBuilder.append("datp.in_multiples_of_deposit_term as inMultiplesOfDepositTerm, "); + sqlBuilder.append("datp.in_multiples_of_deposit_term_type_enum as inMultiplesOfDepositTermTypeId, "); + sqlBuilder.append("datp.deposit_amount as depositAmount, "); + sqlBuilder.append("datp.maturity_amount as maturityAmount, "); + sqlBuilder.append("datp.maturity_date as maturityDate, "); + sqlBuilder.append("datp.deposit_period as depositPeriod, "); + sqlBuilder.append("datp.deposit_period_frequency_enum as depositPeriodFrequencyTypeId, "); + sqlBuilder.append("datp.on_account_closure_enum as onAccountClosureId, "); + sqlBuilder.append("datp.transfer_interest_to_linked_account as transferInterestToSavings, "); + sqlBuilder.append("datp.transfer_to_savings_account_id as transferToSavingsId, "); + sqlBuilder.append("sp.fd_type as fdType, sp.product_code as productCode, sp.effective_date as effectiveDate, "); + sqlBuilder.append("sp.effective_to as effectiveTo, sp.end_date as endDate, sp.introducer_required as introducerRequired, "); + sqlBuilder.append("sp.share_linking_enabled as shareLinkingEnabled, "); + sqlBuilder.append("COALESCE(sa.nomination_facility, sp.nomination_facility) as nominationFacility, "); + sqlBuilder.append("sp.dormancy_alert as dormancyAlert, sp.is_dormancy_tracking_active as isDormancyTrackingActive, "); + sqlBuilder.append("sp.days_to_inactive as daysToInactive, sp.days_to_dormancy as daysToDormancy, "); + sqlBuilder.append("sp.days_to_escheat as daysToEscheat, sp.interest_calculate_selector as interestCalculateSelector, "); + sqlBuilder.append("icv.code_value as interestCalculationTypeName, sp.average_period as averagePeriod, "); + sqlBuilder.append("sp.auto_post as autoPost, sp.frequency_fix_day as frequencyFixDay, "); + sqlBuilder.append("sp.last_interest_date as lastInterestDate, sp.enable_lockin_period as enableLockinPeriod, "); + sqlBuilder.append("sp.balance_frequency as balanceFrequency, "); + sqlBuilder.append("sp.account_closed_zero_balance_in_day_closing as accountClosedZeroBalanceInDayClosing, "); + sqlBuilder.append("sp.balance_swap_option as balanceSwapOption, sp.msme as msme, "); + sqlBuilder.append("COALESCE(sa.auto_debit_facility, sp.auto_debit_facility) as autoDebitFacility, "); + sqlBuilder.append("sp.loan_facility as loanFacility, sp.premature_withdrawal_option as prematureWithdrawalOption, "); + sqlBuilder.append("sp.tenure_flexibility as tenureFlexibility, sp.lien_marked as lienMarked, "); + sqlBuilder.append("sp.maturity_instructions as maturityInstructions, sp.credit_maturity_proceeds as creditMaturityProceeds, "); + sqlBuilder.append("sp.penalty_grace_period_missed_installments as penaltyGracePeriodMissedInstallments, "); + sqlBuilder.append("sp.panel_request_applicable as panelRequestApplicable, "); + sqlBuilder.append("sp.no_penalty_nominee_heir_closure as noPenaltyNomineeHeirClosure, "); + sqlBuilder.append("sp.deceased_settlement as deceasedSettlement, sp.allow_debit_txn_after_due_date as allowDebitTxnAfterDueDate "); + + sqlBuilder.append(super.selectTablesSql()); + + this.schemaSql = sqlBuilder.toString(); + } + + @Override + public String schema() { + return this.schemaSql; + } + + @Override + public FixedDepositAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { + + final DepositAccountData depositAccountData = super.mapRow(rs); + final boolean preClosurePenalApplicable = rs.getBoolean(PRE_CLOSURE_PENAL_APPLICABLE); + final BigDecimal preClosurePenalInterest = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, PRE_CLOSURE_PENAL_INTEREST); + final Integer preClosurePenalInterestOnTypeId = JdbcSupport.getInteger(rs, PRE_CLOSURE_PENAL_INTEREST_ON_ID); + final EnumOptionData preClosurePenalInterestOnType = (preClosurePenalInterestOnTypeId == null) ? null + : SavingsEnumerations.preClosurePenaltyInterestOnType(preClosurePenalInterestOnTypeId); + + final Integer minDepositTerm = JdbcSupport.getInteger(rs, MIN_DEPOSIT_TERM); + final Integer maxDepositTerm = JdbcSupport.getInteger(rs, MAX_DEPOSIT_TERM); + final Integer minDepositTermTypeId = JdbcSupport.getInteger(rs, MIN_DEPOSIT_TERM_TYPE_ID); + final EnumOptionData minDepositTermType = (minDepositTermTypeId == null) ? null + : SavingsEnumerations.depositTermFrequencyType(minDepositTermTypeId); + final Integer maxDepositTermTypeId = JdbcSupport.getInteger(rs, MAX_DEPOSIT_TERM_TYPE_ID); + final EnumOptionData maxDepositTermType = (maxDepositTermTypeId == null) ? null + : SavingsEnumerations.depositTermFrequencyType(maxDepositTermTypeId); + final Integer inMultiplesOfDepositTerm = JdbcSupport.getInteger(rs, IN_MULTIPLES_OF_DEPOSIT_TERM); + final Integer inMultiplesOfDepositTermTypeId = JdbcSupport.getInteger(rs, IN_MULTIPLES_OF_DEPOSIT_TERM_TYPE_ID); + final EnumOptionData inMultiplesOfDepositTermType = (inMultiplesOfDepositTermTypeId == null) ? null + : SavingsEnumerations.depositTermFrequencyType(inMultiplesOfDepositTermTypeId); + + final BigDecimal depositAmount = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, DEPOSIT_AMOUNT); + final BigDecimal maturityAmount = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, MATURITY_AMOUNT); + final LocalDate maturityDate = JdbcSupport.getLocalDate(rs, MATURITY_DATE); + final Integer depositPeriod = JdbcSupport.getInteger(rs, DEPOSIT_PERIOD); + final Integer depositPeriodFrequencyTypeId = JdbcSupport.getInteger(rs, DEPOSIT_PERIOD_FREQUENCY_TYPE_ID); + final EnumOptionData depositPeriodFrequencyType = (depositPeriodFrequencyTypeId == null) ? null + : SavingsEnumerations.depositPeriodFrequency(depositPeriodFrequencyTypeId); + + final Integer onAccountClosureId = JdbcSupport.getInteger(rs, ON_ACCOUNT_CLOSURE_ID); + final EnumOptionData onAccountClosureType = (onAccountClosureId == null) ? null + : SavingsEnumerations.depositAccountOnClosureType(onAccountClosureId); + final Boolean transferInterestToSavings = rs.getBoolean(TRANSFER_INTEREST_TO_SAVINGS); + + final Long transferToSavingsId = JdbcSupport.getLong(rs, TRANSFER_TO_SAVINGS_ID); + + final FixedDepositAccountData result = FixedDepositAccountData.instance(depositAccountData, preClosurePenalApplicable, preClosurePenalInterest, + preClosurePenalInterestOnType, minDepositTerm, maxDepositTerm, minDepositTermType, maxDepositTermType, + inMultiplesOfDepositTerm, inMultiplesOfDepositTermType, depositAmount, maturityAmount, maturityDate, depositPeriod, + depositPeriodFrequencyType, onAccountClosureType, transferInterestToSavings, transferToSavingsId); + result.setCbsProductFields(rs.getString("accountType"), rs.getString("customerSegment"), rs.getString("fdType"), rs.getString("productCode"), + JdbcSupport.getLocalDate(rs, "effectiveDate"), JdbcSupport.getLocalDate(rs, "effectiveTo"), + JdbcSupport.getLocalDate(rs, "endDate"), rs.getBoolean("introducerRequired"), rs.getBoolean("shareLinkingEnabled"), + rs.getBoolean("nominationFacility"), rs.getBoolean("dormancyAlert"), rs.getBoolean("isDormancyTrackingActive"), + JdbcSupport.getLong(rs, "daysToInactive"), JdbcSupport.getLong(rs, "daysToDormancy"), + JdbcSupport.getLong(rs, "daysToEscheat"), JdbcSupport.getInteger(rs, "interestCalculateSelector"), + rs.getString("interestCalculationTypeName"), rs.getString("averagePeriod"), rs.getBoolean("autoPost"), + JdbcSupport.getInteger(rs, "frequencyFixDay"), JdbcSupport.getLocalDate(rs, "lastInterestDate"), + rs.getBoolean("enableLockinPeriod"), rs.getString("balanceFrequency"), + rs.getBoolean("accountClosedZeroBalanceInDayClosing"), rs.getBoolean("balanceSwapOption"), rs.getBoolean("msme"), + rs.getBoolean("autoDebitFacility"), rs.getBoolean("loanFacility"), rs.getBoolean("prematureWithdrawalOption"), + rs.getBoolean("tenureFlexibility"), rs.getBoolean("lienMarked"), rs.getBoolean("maturityInstructions"), + rs.getBoolean("creditMaturityProceeds"), rs.getBoolean("penaltyGracePeriodMissedInstallments"), + rs.getBoolean("panelRequestApplicable"), rs.getBoolean("noPenaltyNomineeHeirClosure"), + rs.getBoolean("deceasedSettlement"), rs.getBoolean("allowDebitTxnAfterDueDate")); + return result; + } + } + + private static class RecurringDepositAccountMapper extends DepositAccountMapper { + + public static final String PRE_CLOSURE_PENAL_APPLICABLE = "preClosurePenalApplicable"; + public static final String PRE_CLOSURE_PENAL_INTEREST = "preClosurePenalInterest"; + public static final String PRE_CLOSURE_PENAL_INTEREST_ON_ID = "preClosurePenalInterestOnId"; + public static final String MIN_DEPOSIT_TERM = "minDepositTerm"; + public static final String MAX_DEPOSIT_TERM = "maxDepositTerm"; + public static final String MIN_DEPOSIT_TERM_TYPE_ID = "minDepositTermTypeId"; + public static final String MAX_DEPOSIT_TERM_TYPE_ID = "maxDepositTermTypeId"; + public static final String IN_MULTIPLES_OF_DEPOSIT_TERM = "inMultiplesOfDepositTerm"; + public static final String IN_MULTIPLES_OF_DEPOSIT_TERM_TYPE_ID = "inMultiplesOfDepositTermTypeId"; + public static final String DEPOSIT_AMOUNT = "depositAmount"; + public static final String MATURITY_AMOUNT = "maturityAmount"; + public static final String MATURITY_DATE = "maturityDate"; + public static final String DEPOSIT_PERIOD = "depositPeriod"; + public static final String DEPOSIT_PERIOD_FREQUENCY_TYPE_ID = "depositPeriodFrequencyTypeId"; + public static final String MANDATORY_RECOMMENDED_DEPOSIT_AMOUNT = "mandatoryRecommendedDepositAmount"; + public static final String TOTAL_OVERDUE_AMOUNT = "totalOverdueAmount"; + public static final String NO_OF_OVERDUE_INSTALLMENTS = "noOfOverdueInstallments"; + public static final String IS_MANDATORY_DEPOSIT = "isMandatoryDeposit"; + public static final String ALLOW_WITHDRAWAL = "allowWithdrawal"; + public static final String ADJUST_ADVANCE_TOWARDS_FUTURE_PAYMENTS = "adjustAdvanceTowardsFuturePayments"; + public static final String IS_CALENDAR_INHERITED = "isCalendarInherited"; + public static final String ON_ACCOUNT_CLOSURE_ID = "onAccountClosureId"; + public static final String EXPECTED_FIRST_DEPOSIT_ON_DATE = "expectedFirstDepositOnDate"; + private final String schemaSql; + + RecurringDepositAccountMapper() { + final StringBuilder sqlBuilder = new StringBuilder(400); + sqlBuilder.append(this.selectFieldsSql()); + + sqlBuilder.append(", datp.pre_closure_penal_applicable as preClosurePenalApplicable, "); + sqlBuilder.append("datp.pre_closure_penal_interest as preClosurePenalInterest, "); + sqlBuilder.append("datp.pre_closure_penal_interest_on_enum as preClosurePenalInterestOnId, "); + sqlBuilder.append("dard.mandatory_recommended_deposit_amount as mandatoryRecommendedDepositAmount, "); + sqlBuilder.append("dard.total_overdue_amount as totalOverdueAmount, "); + sqlBuilder.append("dard.no_of_overdue_installments as noOfOverdueInstallments, "); + sqlBuilder.append("dard.is_mandatory as isMandatoryDeposit, "); + sqlBuilder.append("dard.allow_withdrawal as allowWithdrawal, "); + sqlBuilder.append("dard.adjust_advance_towards_future_payments as adjustAdvanceTowardsFuturePayments, "); + sqlBuilder.append("dard.is_calendar_inherited as isCalendarInherited, "); + sqlBuilder.append("datp.min_deposit_term as minDepositTerm, "); + sqlBuilder.append("datp.max_deposit_term as maxDepositTerm, "); + sqlBuilder.append("datp.min_deposit_term_type_enum as minDepositTermTypeId, "); + sqlBuilder.append("datp.max_deposit_term_type_enum as maxDepositTermTypeId, "); + sqlBuilder.append("datp.in_multiples_of_deposit_term as inMultiplesOfDepositTerm, "); + sqlBuilder.append("datp.in_multiples_of_deposit_term_type_enum as inMultiplesOfDepositTermTypeId, "); + sqlBuilder.append("datp.deposit_amount as depositAmount, "); + sqlBuilder.append("datp.maturity_amount as maturityAmount, "); + sqlBuilder.append("datp.expected_firstdepositon_date as expectedFirstDepositOnDate, "); + sqlBuilder.append("datp.maturity_date as maturityDate, "); + sqlBuilder.append("datp.deposit_period as depositPeriod, "); + sqlBuilder.append("datp.deposit_period_frequency_enum as depositPeriodFrequencyTypeId, "); + sqlBuilder.append("datp.on_account_closure_enum as onAccountClosureId, "); + sqlBuilder.append("sp.product_code as productCode, sp.effective_date as effectiveDate, "); + sqlBuilder.append("sp.effective_to as effectiveTo, sp.end_date as endDate, sp.introducer_required as introducerRequired, "); + sqlBuilder.append("sp.share_linking_enabled as shareLinkingEnabled, "); + sqlBuilder.append("COALESCE(sa.nomination_facility, sp.nomination_facility) as nominationFacility, "); + sqlBuilder.append("sp.dormancy_alert as dormancyAlert, sp.is_dormancy_tracking_active as isDormancyTrackingActive, "); + sqlBuilder.append("sp.days_to_inactive as daysToInactive, sp.days_to_dormancy as daysToDormancy, "); + sqlBuilder.append("sp.days_to_escheat as daysToEscheat, sp.interest_calculate_selector as interestCalculateSelector, "); + sqlBuilder.append("icv.code_value as interestCalculationTypeName, sp.average_period as averagePeriod, "); + sqlBuilder.append("sp.auto_post as autoPost, sp.frequency_fix_day as frequencyFixDay, "); + sqlBuilder.append("sp.last_interest_date as lastInterestDate, sp.enable_lockin_period as enableLockinPeriod, "); + sqlBuilder.append("sp.balance_frequency as balanceFrequency, "); + sqlBuilder.append("sp.account_closed_zero_balance_in_day_closing as accountClosedZeroBalanceInDayClosing, "); + sqlBuilder.append("sp.balance_swap_option as balanceSwapOption, sp.msme as msme, "); + sqlBuilder.append("COALESCE(sa.auto_debit_facility, sp.auto_debit_facility) as autoDebitFacility, "); + sqlBuilder.append("sp.loan_facility as loanFacility, sp.premature_withdrawal_option as prematureWithdrawalOption, "); + sqlBuilder.append("sp.tenure_flexibility as tenureFlexibility, sp.lien_marked as lienMarked, "); + sqlBuilder.append("sp.maturity_instructions as maturityInstructions, sp.credit_maturity_proceeds as creditMaturityProceeds, "); + sqlBuilder.append("sp.penalty_grace_period_missed_installments as penaltyGracePeriodMissedInstallments, "); + sqlBuilder.append("sp.panel_request_applicable as panelRequestApplicable, "); + sqlBuilder.append("sp.no_penalty_nominee_heir_closure as noPenaltyNomineeHeirClosure, "); + sqlBuilder.append("sp.deceased_settlement as deceasedSettlement, sp.allow_debit_txn_after_due_date as allowDebitTxnAfterDueDate "); + + sqlBuilder.append(this.selectTablesSql()); + sqlBuilder.append("left join m_deposit_account_recurring_detail dard on sa.id = dard.savings_account_id "); + + this.schemaSql = sqlBuilder.toString(); + } + + @Override + public String schema() { + return this.schemaSql; + } + + @Override + public RecurringDepositAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { + + final DepositAccountData depositAccountData = super.mapRow(rs); + + final boolean preClosurePenalApplicable = rs.getBoolean(PRE_CLOSURE_PENAL_APPLICABLE); + final BigDecimal preClosurePenalInterest = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, PRE_CLOSURE_PENAL_INTEREST); + final Integer preClosurePenalInterestOnTypeId = JdbcSupport.getInteger(rs, PRE_CLOSURE_PENAL_INTEREST_ON_ID); + final EnumOptionData preClosurePenalInterestOnType = (preClosurePenalInterestOnTypeId == null) ? null + : SavingsEnumerations.preClosurePenaltyInterestOnType(preClosurePenalInterestOnTypeId); + final Integer minDepositTerm = JdbcSupport.getInteger(rs, MIN_DEPOSIT_TERM); + final Integer maxDepositTerm = JdbcSupport.getInteger(rs, MAX_DEPOSIT_TERM); + final Integer minDepositTermTypeId = JdbcSupport.getInteger(rs, MIN_DEPOSIT_TERM_TYPE_ID); + final EnumOptionData minDepositTermType = (minDepositTermTypeId == null) ? null + : SavingsEnumerations.depositTermFrequencyType(minDepositTermTypeId); + final Integer maxDepositTermTypeId = JdbcSupport.getInteger(rs, MAX_DEPOSIT_TERM_TYPE_ID); + final EnumOptionData maxDepositTermType = (maxDepositTermTypeId == null) ? null + : SavingsEnumerations.depositTermFrequencyType(maxDepositTermTypeId); + final Integer inMultiplesOfDepositTerm = JdbcSupport.getInteger(rs, IN_MULTIPLES_OF_DEPOSIT_TERM); + final Integer inMultiplesOfDepositTermTypeId = JdbcSupport.getInteger(rs, IN_MULTIPLES_OF_DEPOSIT_TERM_TYPE_ID); + final EnumOptionData inMultiplesOfDepositTermType = (inMultiplesOfDepositTermTypeId == null) ? null + : SavingsEnumerations.depositTermFrequencyType(inMultiplesOfDepositTermTypeId); + + final BigDecimal depositAmount = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, DEPOSIT_AMOUNT); + final BigDecimal maturityAmount = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, MATURITY_AMOUNT); + final LocalDate maturityDate = JdbcSupport.getLocalDate(rs, MATURITY_DATE); + final Integer depositPeriod = JdbcSupport.getInteger(rs, DEPOSIT_PERIOD); + final Integer depositPeriodFrequencyTypeId = JdbcSupport.getInteger(rs, DEPOSIT_PERIOD_FREQUENCY_TYPE_ID); + final EnumOptionData depositPeriodFrequencyType = (depositPeriodFrequencyTypeId == null) ? null + : SavingsEnumerations.depositPeriodFrequency(depositPeriodFrequencyTypeId); + final BigDecimal mandatoryRecommendedDepositAmount = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, + MANDATORY_RECOMMENDED_DEPOSIT_AMOUNT); + final BigDecimal totalOverdueAmount = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, TOTAL_OVERDUE_AMOUNT); + final Integer noOfOverdueInstallments = JdbcSupport.getInteger(rs, NO_OF_OVERDUE_INSTALLMENTS); + final boolean isMandatoryDeposit = rs.getBoolean(IS_MANDATORY_DEPOSIT); + final boolean allowWithdrawal = rs.getBoolean(ALLOW_WITHDRAWAL); + final boolean adjustAdvanceTowardsFuturePayments = rs.getBoolean(ADJUST_ADVANCE_TOWARDS_FUTURE_PAYMENTS); + final boolean isCalendarInherited = rs.getBoolean(IS_CALENDAR_INHERITED); + + final Integer onAccountClosureId = JdbcSupport.getInteger(rs, ON_ACCOUNT_CLOSURE_ID); + final EnumOptionData onAccountClosureType = (onAccountClosureId == null) ? null + : SavingsEnumerations.depositAccountOnClosureType(onAccountClosureId); + final LocalDate expectedFirstDepositOnDate = JdbcSupport.getLocalDate(rs, EXPECTED_FIRST_DEPOSIT_ON_DATE); + + final RecurringDepositAccountData result = RecurringDepositAccountData.instance(depositAccountData, + preClosurePenalApplicable, preClosurePenalInterest, preClosurePenalInterestOnType, minDepositTerm, maxDepositTerm, + minDepositTermType, maxDepositTermType, inMultiplesOfDepositTerm, inMultiplesOfDepositTermType, depositAmount, + maturityAmount, maturityDate, depositPeriod, depositPeriodFrequencyType, mandatoryRecommendedDepositAmount, + onAccountClosureType, expectedFirstDepositOnDate, totalOverdueAmount, noOfOverdueInstallments, isMandatoryDeposit, + allowWithdrawal, adjustAdvanceTowardsFuturePayments, isCalendarInherited); + + result.setCbsProductFields(rs.getString("accountType"), rs.getString("customerSegment"), rs.getString("productCode"), + JdbcSupport.getLocalDate(rs, "effectiveDate"), JdbcSupport.getLocalDate(rs, "effectiveTo"), + JdbcSupport.getLocalDate(rs, "endDate"), rs.getBoolean("introducerRequired"), rs.getBoolean("shareLinkingEnabled"), + rs.getBoolean("nominationFacility"), rs.getBoolean("dormancyAlert"), rs.getBoolean("isDormancyTrackingActive"), + JdbcSupport.getInteger(rs, "daysToInactive"), JdbcSupport.getInteger(rs, "daysToDormancy"), + JdbcSupport.getInteger(rs, "daysToEscheat"), JdbcSupport.getInteger(rs, "interestCalculateSelector"), + rs.getString("interestCalculationTypeName"), rs.getString("averagePeriod"), rs.getBoolean("autoPost"), + JdbcSupport.getInteger(rs, "frequencyFixDay"), JdbcSupport.getLocalDate(rs, "lastInterestDate"), + rs.getBoolean("enableLockinPeriod"), rs.getString("balanceFrequency"), + rs.getBoolean("accountClosedZeroBalanceInDayClosing"), rs.getBoolean("balanceSwapOption"), rs.getBoolean("msme"), + rs.getBoolean("autoDebitFacility"), rs.getBoolean("loanFacility"), rs.getBoolean("prematureWithdrawalOption"), + rs.getBoolean("tenureFlexibility"), rs.getBoolean("lienMarked"), rs.getBoolean("maturityInstructions"), + rs.getBoolean("creditMaturityProceeds"), rs.getBoolean("penaltyGracePeriodMissedInstallments"), + rs.getBoolean("panelRequestApplicable"), rs.getBoolean("noPenaltyNomineeHeirClosure"), + rs.getBoolean("deceasedSettlement"), rs.getBoolean("allowDebitTxnAfterDueDate")); + + return result; + + } + } + + private static final class DepositAccountLookupMapper implements RowMapper { + + public static final String ID = "id"; + public static final String ACCOUNT_NUMBER = "accountNumber"; + public static final String DEPOSIT_TYPE_ID = "depositTypeId"; + + public String schema() { + return " sa.id as id, sa.account_no as accountNumber, sa.deposit_type_enum as depositTypeId from m_savings_account sa "; + } + + @Override + public DepositAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { + + final Long id = rs.getLong(ID); + final String name = rs.getString(ACCOUNT_NUMBER); + final Integer depositTypeId = JdbcSupport.getInteger(rs, DEPOSIT_TYPE_ID); + final EnumOptionData depositType = (depositTypeId == null) ? null : SavingsEnumerations.depositType(depositTypeId); + + return DepositAccountData.lookup(id, name, depositType); + } + } + + private static final class SavingsAccountTransactionsMapper implements RowMapper { + + public static final String TRANSACTION_ID = "transactionId"; + public static final String TRANSACTION_TYPE = "transactionType"; + public static final String TRANSACTION_DATE = "transactionDate"; + public static final String TRANSACTION_AMOUNT = "transactionAmount"; + public static final String RUNNING_BALANCE = "runningBalance"; + public static final String REVERSED = "reversed"; + public static final String SAVINGS_ID = "savingsId"; + public static final String ACCOUNT_NO = "accountNo"; + public static final String PAYMENT_TYPE = "paymentType"; + public static final String PAYMENT_TYPE_NAME = "paymentTypeName"; + public static final String ACCOUNT_NUMBER = "accountNumber"; + public static final String CHECK_NUMBER = "checkNumber"; + public static final String ROUTING_CODE = "routingCode"; + public static final String RECEIPT_NUMBER = "receiptNumber"; + public static final String BANK_NUMBER = "bankNumber"; + public static final String CURRENCY_CODE = "currencyCode"; + public static final String CURRENCY_NAME = "currencyName"; + public static final String CURRENCY_NAME_CODE = "currencyNameCode"; + public static final String CURRENCY_DISPLAY_SYMBOL = "currencyDisplaySymbol"; + public static final String CURRENCY_DIGITS = "currencyDigits"; + public static final String IN_MULTIPLES_OF = "inMultiplesOf"; + public static final String FROM_TRANSFER_ID = "fromTransferId"; + public static final String TO_TRANSFER_ID = "toTransferId"; + public static final String FROM_TRANSFER_DATE = "fromTransferDate"; + public static final String FROM_TRANSFER_AMOUNT = "fromTransferAmount"; + public static final String FROM_TRANSFER_REVERSED = "fromTransferReversed"; + public static final String FROM_TRANSFER_DESCRIPTION = "fromTransferDescription"; + public static final String TO_TRANSFER_DATE = "toTransferDate"; + public static final String TO_TRANSFER_AMOUNT = "toTransferAmount"; + public static final String TO_TRANSFER_REVERSED = "toTransferReversed"; + public static final String TO_TRANSFER_DESCRIPTION = "toTransferDescription"; + public static final String SUBMITTED_BY_USERNAME = "submittedByUsername"; + public static final String SUBMITTED_ON_DATE = "submittedOnDate"; + private final String schemaSql; + + SavingsAccountTransactionsMapper() { + + final StringBuilder sqlBuilder = new StringBuilder(400); + sqlBuilder.append("tr.id as transactionId, tr.transaction_type_enum as transactionType, "); + sqlBuilder.append("tr.transaction_date as transactionDate, tr.amount as transactionAmount,"); + sqlBuilder.append("tr.running_balance_derived as runningBalance, tr.is_reversed as reversed,"); + sqlBuilder.append("tr.submitted_on_date as submittedOnDate,"); + sqlBuilder.append("fromtran.id as fromTransferId, fromtran.is_reversed as fromTransferReversed,"); + sqlBuilder.append("fromtran.transaction_date as fromTransferDate, fromtran.amount as fromTransferAmount,"); + sqlBuilder.append("fromtran.description as fromTransferDescription,"); + sqlBuilder.append("totran.id as toTransferId, totran.is_reversed as toTransferReversed,"); + sqlBuilder.append("totran.transaction_date as toTransferDate, totran.amount as toTransferAmount,"); + sqlBuilder.append("totran.description as toTransferDescription,"); + sqlBuilder.append("sa.id as savingsId, sa.account_no as accountNo,"); + sqlBuilder.append(" au.username as submittedByUsername, "); + sqlBuilder.append("pd.payment_type_id as paymentType,pd.account_number as accountNumber,pd.check_number as checkNumber, "); + sqlBuilder.append("pd.receipt_number as receiptNumber, pd.bank_number as bankNumber,pd.routing_code as routingCode, "); + sqlBuilder.append( + "sa.currency_code as currencyCode, sa.currency_digits as currencyDigits, 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("pt.value as paymentTypeName "); + sqlBuilder.append("from m_savings_account sa "); + sqlBuilder.append("join m_savings_account_transaction tr on tr.savings_account_id = sa.id "); + sqlBuilder.append("join m_currency curr on curr.code = sa.currency_code "); + sqlBuilder.append("left join m_account_transfer_transaction fromtran on fromtran.from_savings_transaction_id = tr.id "); + sqlBuilder.append("left join m_account_transfer_transaction totran on totran.to_savings_transaction_id = tr.id "); + sqlBuilder.append("left join m_payment_detail pd on tr.payment_detail_id = pd.id "); + sqlBuilder.append("left join m_payment_type pt on pd.payment_type_id = pt.id "); + sqlBuilder.append("left join m_appuser au on au.id = tr." + CREATED_BY_DB_FIELD); + this.schemaSql = sqlBuilder.toString(); + } + + public String schema() { + return this.schemaSql; + } + + @Override + public SavingsAccountTransactionData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { + final Long id = rs.getLong(TRANSACTION_ID); + final int transactionTypeInt = JdbcSupport.getInteger(rs, TRANSACTION_TYPE); + final SavingsAccountTransactionEnumData transactionType = SavingsEnumerations.transactionType(transactionTypeInt); + + final LocalDate date = JdbcSupport.getLocalDate(rs, TRANSACTION_DATE); + final LocalDate submittedOnDate = JdbcSupport.getLocalDate(rs, SUBMITTED_ON_DATE); + final BigDecimal amount = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, TRANSACTION_AMOUNT); + final BigDecimal outstandingChargeAmount = null; + final BigDecimal runningBalance = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, RUNNING_BALANCE); + final boolean reversed = rs.getBoolean(REVERSED); + + final Long savingsId = rs.getLong(SAVINGS_ID); + final String accountNo = rs.getString(ACCOUNT_NO); + + PaymentDetailData paymentDetailData = null; + if (transactionType.isDepositOrWithdrawal()) { + final Long paymentTypeId = JdbcSupport.getLong(rs, PAYMENT_TYPE); + if (paymentTypeId != null) { + final String typeName = rs.getString(PAYMENT_TYPE_NAME); + final PaymentTypeData paymentType = PaymentTypeData.builder().id(paymentTypeId).name(typeName).build(); + final String accountNumber = rs.getString(ACCOUNT_NUMBER); + final String checkNumber = rs.getString(CHECK_NUMBER); + final String routingCode = rs.getString(ROUTING_CODE); + final String receiptNumber = rs.getString(RECEIPT_NUMBER); + final String bankNumber = rs.getString(BANK_NUMBER); + paymentDetailData = new PaymentDetailData(id, paymentType, accountNumber, checkNumber, routingCode, receiptNumber, + bankNumber); + } + } + + final String currencyCode = rs.getString(CURRENCY_CODE); + final String currencyName = rs.getString(CURRENCY_NAME); + final String currencyNameCode = rs.getString(CURRENCY_NAME_CODE); + final String currencyDisplaySymbol = rs.getString(CURRENCY_DISPLAY_SYMBOL); + final Integer currencyDigits = JdbcSupport.getInteger(rs, CURRENCY_DIGITS); + final Integer inMultiplesOf = JdbcSupport.getInteger(rs, IN_MULTIPLES_OF); + final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMultiplesOf, currencyDisplaySymbol, + currencyNameCode); + + AccountTransferData transfer = null; + final Long fromTransferId = JdbcSupport.getLong(rs, FROM_TRANSFER_ID); + final Long toTransferId = JdbcSupport.getLong(rs, TO_TRANSFER_ID); + if (fromTransferId != null) { + final LocalDate fromTransferDate = JdbcSupport.getLocalDate(rs, FROM_TRANSFER_DATE); + final BigDecimal fromTransferAmount = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, FROM_TRANSFER_AMOUNT); + final boolean fromTransferReversed = rs.getBoolean(FROM_TRANSFER_REVERSED); + final String fromTransferDescription = rs.getString(FROM_TRANSFER_DESCRIPTION); + + transfer = AccountTransferData.transferBasicDetails(fromTransferId, currency, fromTransferAmount, fromTransferDate, + fromTransferDescription, fromTransferReversed); + } else if (toTransferId != null) { + final LocalDate toTransferDate = JdbcSupport.getLocalDate(rs, TO_TRANSFER_DATE); + final BigDecimal toTransferAmount = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, TO_TRANSFER_AMOUNT); + final boolean toTransferReversed = rs.getBoolean(TO_TRANSFER_REVERSED); + final String toTransferDescription = rs.getString(TO_TRANSFER_DESCRIPTION); + + transfer = AccountTransferData.transferBasicDetails(toTransferId, currency, toTransferAmount, toTransferDate, + toTransferDescription, toTransferReversed); + } + final boolean postInterestAsOn = false; + final String submittedByUsername = rs.getString(SUBMITTED_BY_USERNAME); + final String note = null; + return SavingsAccountTransactionData.create(id, transactionType, paymentDetailData, savingsId, accountNo, date, currency, + amount, outstandingChargeAmount, runningBalance, reversed, transfer, postInterestAsOn, submittedByUsername, note, + submittedOnDate); + } + } + + private abstract static class DepositAccountTemplateMapper implements RowMapper { + + public static final String DEPOSIT_TYPE_ID = "depositTypeId"; + public static final String PRODUCT_ID = "productId"; + public static final String PRODUCT_NAME = "productName"; + public static final String CURRENCY_CODE = "currencyCode"; + public static final String CURRENCY_NAME = "currencyName"; + public static final String CURRENCY_NAME_CODE = "currencyNameCode"; + public static final String CURRENCY_DISPLAY_SYMBOL = "currencyDisplaySymbol"; + public static final String CURRENCY_DIGITS = "currencyDigits"; + public static final String IN_MULTIPLES_OF = "inMultiplesOf"; + public static final String NOMINAL_ANNUAL_ITEREST_RATE = "nominalAnnualIterestRate"; + public static final String INTEREST_COMPOUNDING_PERIOD_TYPE = "interestCompoundingPeriodType"; + public static final String INTEREST_POSTING_PERIOD_TYPE = "interestPostingPeriodType"; + public static final String INTEREST_CALCULATION_TYPE = "interestCalculationType"; + public static final String INTEREST_CALCULATION_DAYS_IN_YEAR_TYPE = "interestCalculationDaysInYearType"; + public static final String MIN_REQUIRED_OPENING_BALANCE = "minRequiredOpeningBalance"; + public static final String LOCKIN_PERIOD_FREQUENCY = "lockinPeriodFrequency"; + public static final String LOCKIN_PERIOD_FREQUENCY_TYPE = "lockinPeriodFrequencyType"; + public static final String WITHDRAWAL_FEE_FOR_TRANSFERS = "withdrawalFeeForTransfers"; + public static final String MIN_BALANCE_FOR_INTEREST_CALCULATION = "minBalanceForInterestCalculation"; + public static final String WITH_HOLD_TAX = "withHoldTax"; + public static final String TAX_GROUP_ID = "taxGroupId"; + public static final String TAX_GROUP_NAME = "taxGroupName"; + private final String selectFieldsSql; + private final String selectTablesSql; + + private final ClientData client; + private final GroupGeneralData group; + + protected DepositAccountTemplateMapper(final ClientData client, final GroupGeneralData group) { + this.client = client; + this.group = group; + + final StringBuilder selectFieldsSqlBuilder = new StringBuilder(400); + selectFieldsSqlBuilder.append("sa.id as productId, sa.name as productName, "); + selectFieldsSqlBuilder.append( + "sa.currency_code as currencyCode, sa.currency_digits as currencyDigits, sa.currency_multiplesof as inMultiplesOf, "); + selectFieldsSqlBuilder.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, "); + selectFieldsSqlBuilder.append("curr.display_symbol as currencyDisplaySymbol, "); + selectFieldsSqlBuilder.append("sa.nominal_annual_interest_rate as nominalAnnualIterestRate, "); + selectFieldsSqlBuilder.append("sa.interest_compounding_period_enum as interestCompoundingPeriodType, "); + selectFieldsSqlBuilder.append("sa.interest_posting_period_enum as interestPostingPeriodType, "); + selectFieldsSqlBuilder.append("sa.interest_calculation_type_enum as interestCalculationType, "); + selectFieldsSqlBuilder.append("sa.interest_calculation_days_in_year_type_enum as interestCalculationDaysInYearType, "); + selectFieldsSqlBuilder.append("sa.min_required_opening_balance as minRequiredOpeningBalance, "); + selectFieldsSqlBuilder.append("sa.lockin_period_frequency as lockinPeriodFrequency,"); + selectFieldsSqlBuilder.append("sa.lockin_period_frequency_enum as lockinPeriodFrequencyType, "); + selectFieldsSqlBuilder.append("sa.withdrawal_fee_for_transfer as withdrawalFeeForTransfers, "); + selectFieldsSqlBuilder.append("sa.deposit_type_enum as depositTypeId, "); + selectFieldsSqlBuilder.append("sa.min_balance_for_interest_calculation as minBalanceForInterestCalculation, "); + selectFieldsSqlBuilder.append("sa.withhold_tax as withHoldTax,"); + selectFieldsSqlBuilder.append("tg.id as taxGroupId, tg.name as taxGroupName "); + + this.selectFieldsSql = selectFieldsSqlBuilder.toString(); + + final StringBuilder selectTablesSqlBuilder = new StringBuilder(400); + selectTablesSqlBuilder.append("from m_savings_product sa "); + selectTablesSqlBuilder.append("left join m_deposit_product_term_and_preclosure dptp on sa.id = dptp.savings_product_id "); + selectTablesSqlBuilder.append("join m_currency curr on curr.code = sa.currency_code "); + selectTablesSqlBuilder.append("left join m_tax_group tg on tg.id = sa.tax_group_id "); + + this.selectTablesSql = selectTablesSqlBuilder.toString(); + } + + public String selectFieldsSql() { + return this.selectFieldsSql; + } + + public String selectTablesSql() { + return this.selectTablesSql; + } + + public abstract String schema(); + + public DepositAccountData mapRow(final ResultSet rs) throws SQLException { + + final Integer depositTypeId = JdbcSupport.getInteger(rs, DEPOSIT_TYPE_ID); + final EnumOptionData depositType = (depositTypeId == null) ? null : SavingsEnumerations.depositType(depositTypeId); + final Long productId = rs.getLong(PRODUCT_ID); + final String productName = rs.getString(PRODUCT_NAME); + + final String currencyCode = rs.getString(CURRENCY_CODE); + final String currencyName = rs.getString(CURRENCY_NAME); + final String currencyNameCode = rs.getString(CURRENCY_NAME_CODE); + final String currencyDisplaySymbol = rs.getString(CURRENCY_DISPLAY_SYMBOL); + final Integer currencyDigits = JdbcSupport.getInteger(rs, CURRENCY_DIGITS); + final Integer inMultiplesOf = JdbcSupport.getInteger(rs, IN_MULTIPLES_OF); + final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMultiplesOf, currencyDisplaySymbol, + currencyNameCode); + + final BigDecimal nominalAnnualIterestRate = rs.getBigDecimal(NOMINAL_ANNUAL_ITEREST_RATE); + + final EnumOptionData interestCompoundingPeriodType = SavingsEnumerations.compoundingInterestPeriodType( + SavingsCompoundingInterestPeriodType.fromInt(JdbcSupport.getInteger(rs, INTEREST_COMPOUNDING_PERIOD_TYPE))); + + final EnumOptionData interestPostingPeriodType = SavingsEnumerations.interestPostingPeriodType( + SavingsPostingInterestPeriodType.fromInt(JdbcSupport.getInteger(rs, INTEREST_POSTING_PERIOD_TYPE))); + + final EnumOptionData interestCalculationType = SavingsEnumerations + .interestCalculationType(SavingsInterestCalculationType.fromInt(JdbcSupport.getInteger(rs, INTEREST_CALCULATION_TYPE))); + + final EnumOptionData interestCalculationDaysInYearType = SavingsEnumerations.interestCalculationDaysInYearType( + SavingsInterestCalculationDaysInYearType.fromInt(JdbcSupport.getInteger(rs, INTEREST_CALCULATION_DAYS_IN_YEAR_TYPE))); + + final BigDecimal minRequiredOpeningBalance = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, MIN_REQUIRED_OPENING_BALANCE); + + final Integer lockinPeriodFrequency = JdbcSupport.getInteger(rs, LOCKIN_PERIOD_FREQUENCY); + EnumOptionData lockinPeriodFrequencyType = null; + final Integer lockinPeriodFrequencyTypeValue = JdbcSupport.getInteger(rs, LOCKIN_PERIOD_FREQUENCY_TYPE); + if (lockinPeriodFrequencyTypeValue != null) { + final SavingsPeriodFrequencyType lockinPeriodType = SavingsPeriodFrequencyType.fromInt(lockinPeriodFrequencyTypeValue); + lockinPeriodFrequencyType = SavingsEnumerations.lockinPeriodFrequencyType(lockinPeriodType); + } + + final boolean withdrawalFeeForTransfers = rs.getBoolean(WITHDRAWAL_FEE_FOR_TRANSFERS); + final BigDecimal minBalanceForInterestCalculation = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, + MIN_BALANCE_FOR_INTEREST_CALCULATION); + + Long clientId = null; + String clientName = null; + if (this.client != null) { + clientId = this.client.getId(); + clientName = this.client.getDisplayName(); + } + + Long groupId = null; + String groupName = null; + if (this.group != null) { + groupId = this.group.getId(); + groupName = this.group.getName(); + } + + final Long fieldOfficerId = null; + final String fieldOfficerName = null; + final SavingsAccountStatusEnumData status = null; + final SavingsAccountSummaryData summary = null; + final SavingsAccountApplicationTimelineData timeline = SavingsAccountApplicationTimelineData.templateDefault(); + + final boolean withHoldTax = rs.getBoolean(WITH_HOLD_TAX); + final Long taxGroupId = JdbcSupport.getLong(rs, TAX_GROUP_ID); + final String taxGroupName = rs.getString(TAX_GROUP_NAME); + TaxGroupData taxGroupData = null; + if (taxGroupId != null) { + taxGroupData = TaxGroupData.lookup(taxGroupId, taxGroupName); + } + + return DepositAccountData.instance(null, null, null, groupId, groupName, clientId, clientName, productId, productName, + fieldOfficerId, fieldOfficerName, status, timeline, currency, nominalAnnualIterestRate, interestCompoundingPeriodType, + interestPostingPeriodType, interestCalculationType, interestCalculationDaysInYearType, minRequiredOpeningBalance, + lockinPeriodFrequency, lockinPeriodFrequencyType, withdrawalFeeForTransfers, summary, depositType, + minBalanceForInterestCalculation, withHoldTax, taxGroupData); + } + } + + private static class FixedDepositAccountTemplateMapper extends DepositAccountTemplateMapper { + + public static final String PRE_CLOSURE_PENAL_APPLICABLE = "preClosurePenalApplicable"; + public static final String PRE_CLOSURE_PENAL_INTEREST = "preClosurePenalInterest"; + public static final String PRE_CLOSURE_PENAL_INTEREST_ON_ID = "preClosurePenalInterestOnId"; + public static final String MIN_DEPOSIT_TERM = "minDepositTerm"; + public static final String MAX_DEPOSIT_TERM = "maxDepositTerm"; + public static final String MIN_DEPOSIT_TERM_TYPE_ID = "minDepositTermTypeId"; + public static final String MAX_DEPOSIT_TERM_TYPE_ID = "maxDepositTermTypeId"; + public static final String IN_MULTIPLES_OF_DEPOSIT_TERM = "inMultiplesOfDepositTerm"; + public static final String IN_MULTIPLES_OF_DEPOSIT_TERM_TYPE_ID = "inMultiplesOfDepositTermTypeId"; + private final String schemaSql; + + FixedDepositAccountTemplateMapper(final ClientData client, final GroupGeneralData group) { + super(client, group); + final StringBuilder sqlBuilder = new StringBuilder(400); + sqlBuilder.append(super.selectFieldsSql()); + + sqlBuilder.append(", dptp.pre_closure_penal_applicable as preClosurePenalApplicable, "); + sqlBuilder.append("dptp.pre_closure_penal_interest as preClosurePenalInterest, "); + sqlBuilder.append("dptp.pre_closure_penal_interest_on_enum as preClosurePenalInterestOnId, "); + sqlBuilder.append("sa.account_type as accountType, sa.customer_segment as customerSegment, sa.fd_type as fdType, "); + sqlBuilder.append("sa.product_code as productCode, "); + sqlBuilder.append("sa.effective_date as effectiveDate, sa.effective_to as effectiveTo, sa.end_date as endDate, "); + sqlBuilder.append("sa.introducer_required as introducerRequired, sa.share_linking_enabled as shareLinkingEnabled, "); + sqlBuilder.append("sa.nomination_facility as nominationFacility, sa.dormancy_alert as dormancyAlert, "); + sqlBuilder.append("sa.is_dormancy_tracking_active as isDormancyTrackingActive, "); + sqlBuilder.append("sa.days_to_inactive as daysToInactive, sa.days_to_dormancy as daysToDormancy, "); + sqlBuilder.append("sa.days_to_escheat as daysToEscheat, "); + sqlBuilder.append("sa.interest_calculate_selector as interestCalculateSelector, "); + sqlBuilder.append("icv.code_value as interestCalculationTypeName, "); + sqlBuilder.append("sa.average_period as averagePeriod, "); + sqlBuilder.append("sa.auto_post as autoPost, sa.frequency_fix_day as frequencyFixDay, "); + sqlBuilder.append("sa.last_interest_date as lastInterestDate, sa.enable_lockin_period as enableLockinPeriod, "); + sqlBuilder.append("sa.balance_frequency as balanceFrequency, "); + sqlBuilder.append("sa.account_closed_zero_balance_in_day_closing as accountClosedZeroBalanceInDayClosing, "); + sqlBuilder.append("sa.balance_swap_option as balanceSwapOption, sa.msme as msme, "); + sqlBuilder.append("sa.auto_debit_facility as autoDebitFacility, sa.loan_facility as loanFacility, "); + sqlBuilder.append("sa.premature_withdrawal_option as prematureWithdrawalOption, sa.tenure_flexibility as tenureFlexibility, "); + sqlBuilder.append("sa.lien_marked as lienMarked, sa.maturity_instructions as maturityInstructions, "); + sqlBuilder.append("sa.credit_maturity_proceeds as creditMaturityProceeds, "); + sqlBuilder.append("sa.penalty_grace_period_missed_installments as penaltyGracePeriodMissedInstallments, "); + sqlBuilder.append("sa.panel_request_applicable as panelRequestApplicable, "); + sqlBuilder.append("sa.no_penalty_nominee_heir_closure as noPenaltyNomineeHeirClosure, "); + sqlBuilder.append("sa.deceased_settlement as deceasedSettlement, "); + sqlBuilder.append("sa.allow_debit_txn_after_due_date as allowDebitTxnAfterDueDate, "); + sqlBuilder.append("dptp.min_deposit_term as minDepositTerm, "); + sqlBuilder.append("dptp.max_deposit_term as maxDepositTerm, "); + sqlBuilder.append("dptp.min_deposit_term_type_enum as minDepositTermTypeId, "); + sqlBuilder.append("dptp.max_deposit_term_type_enum as maxDepositTermTypeId, "); + sqlBuilder.append("dptp.in_multiples_of_deposit_term as inMultiplesOfDepositTerm, "); + sqlBuilder.append("dptp.in_multiples_of_deposit_term_type_enum as inMultiplesOfDepositTermTypeId "); + + sqlBuilder.append(super.selectTablesSql()); + sqlBuilder.append("left join m_code_value icv on icv.id = sa.interest_calculate_selector "); + + this.schemaSql = sqlBuilder.toString(); + } + + @Override + public String schema() { + return this.schemaSql; + } + + @Override + public FixedDepositAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { + + final DepositAccountData depositAccountData = super.mapRow(rs); + + final boolean preClosurePenalApplicable = rs.getBoolean(PRE_CLOSURE_PENAL_APPLICABLE); + final BigDecimal preClosurePenalInterest = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, PRE_CLOSURE_PENAL_INTEREST); + final Integer preClosurePenalInterestOnTypeId = JdbcSupport.getInteger(rs, PRE_CLOSURE_PENAL_INTEREST_ON_ID); + final EnumOptionData preClosurePenalInterestOnType = (preClosurePenalInterestOnTypeId == null) ? null + : SavingsEnumerations.preClosurePenaltyInterestOnType(preClosurePenalInterestOnTypeId); + + final Integer minDepositTerm = JdbcSupport.getInteger(rs, MIN_DEPOSIT_TERM); + final Integer maxDepositTerm = JdbcSupport.getInteger(rs, MAX_DEPOSIT_TERM); + final Integer minDepositTermTypeId = JdbcSupport.getInteger(rs, MIN_DEPOSIT_TERM_TYPE_ID); + final EnumOptionData minDepositTermType = (minDepositTermTypeId == null) ? null + : SavingsEnumerations.depositTermFrequencyType(minDepositTermTypeId); + final Integer maxDepositTermTypeId = JdbcSupport.getInteger(rs, MAX_DEPOSIT_TERM_TYPE_ID); + final EnumOptionData maxDepositTermType = (maxDepositTermTypeId == null) ? null + : SavingsEnumerations.depositTermFrequencyType(maxDepositTermTypeId); + final Integer inMultiplesOfDepositTerm = JdbcSupport.getInteger(rs, IN_MULTIPLES_OF_DEPOSIT_TERM); + final Integer inMultiplesOfDepositTermTypeId = JdbcSupport.getInteger(rs, IN_MULTIPLES_OF_DEPOSIT_TERM_TYPE_ID); + final EnumOptionData inMultiplesOfDepositTermType = (inMultiplesOfDepositTermTypeId == null) ? null + : SavingsEnumerations.depositTermFrequencyType(inMultiplesOfDepositTermTypeId); + + final BigDecimal depositAmount = null; + final BigDecimal maturityAmount = null; + final LocalDate maturityDate = null; + final Integer depositPeriod = null; + final EnumOptionData depositPeriodFrequencyType = null; + final EnumOptionData onAccountClosureType = null; + final Boolean transferInterestToSavings = false; + + final FixedDepositAccountData result = FixedDepositAccountData.instance(depositAccountData, preClosurePenalApplicable, + preClosurePenalInterest, preClosurePenalInterestOnType, minDepositTerm, maxDepositTerm, minDepositTermType, + maxDepositTermType, inMultiplesOfDepositTerm, inMultiplesOfDepositTermType, depositAmount, maturityAmount, + maturityDate, depositPeriod, depositPeriodFrequencyType, onAccountClosureType, transferInterestToSavings, null); + result.setCbsProductFields(rs.getString("accountType"), rs.getString("customerSegment"), rs.getString("fdType"), rs.getString("productCode"), + JdbcSupport.getLocalDate(rs, "effectiveDate"), JdbcSupport.getLocalDate(rs, "effectiveTo"), + JdbcSupport.getLocalDate(rs, "endDate"), rs.getBoolean("introducerRequired"), rs.getBoolean("shareLinkingEnabled"), + rs.getBoolean("nominationFacility"), rs.getBoolean("dormancyAlert"), rs.getBoolean("isDormancyTrackingActive"), + JdbcSupport.getLong(rs, "daysToInactive"), JdbcSupport.getLong(rs, "daysToDormancy"), + JdbcSupport.getLong(rs, "daysToEscheat"), JdbcSupport.getInteger(rs, "interestCalculateSelector"), + rs.getString("interestCalculationTypeName"), rs.getString("averagePeriod"), rs.getBoolean("autoPost"), + JdbcSupport.getInteger(rs, "frequencyFixDay"), JdbcSupport.getLocalDate(rs, "lastInterestDate"), + rs.getBoolean("enableLockinPeriod"), rs.getString("balanceFrequency"), + rs.getBoolean("accountClosedZeroBalanceInDayClosing"), rs.getBoolean("balanceSwapOption"), rs.getBoolean("msme"), + rs.getBoolean("autoDebitFacility"), rs.getBoolean("loanFacility"), rs.getBoolean("prematureWithdrawalOption"), + rs.getBoolean("tenureFlexibility"), rs.getBoolean("lienMarked"), rs.getBoolean("maturityInstructions"), + rs.getBoolean("creditMaturityProceeds"), rs.getBoolean("penaltyGracePeriodMissedInstallments"), + rs.getBoolean("panelRequestApplicable"), rs.getBoolean("noPenaltyNomineeHeirClosure"), + rs.getBoolean("deceasedSettlement"), rs.getBoolean("allowDebitTxnAfterDueDate")); + return result; + } + } + + private static class RecurringDepositAccountTemplateMapper extends DepositAccountTemplateMapper { + + public static final String PRE_CLOSURE_PENAL_APPLICABLE = "preClosurePenalApplicable"; + public static final String PRE_CLOSURE_PENAL_INTEREST = "preClosurePenalInterest"; + public static final String PRE_CLOSURE_PENAL_INTEREST_ON_ID = "preClosurePenalInterestOnId"; + public static final String MIN_DEPOSIT_TERM = "minDepositTerm"; + public static final String MAX_DEPOSIT_TERM = "maxDepositTerm"; + public static final String MIN_DEPOSIT_TERM_TYPE_ID = "minDepositTermTypeId"; + public static final String MAX_DEPOSIT_TERM_TYPE_ID = "maxDepositTermTypeId"; + public static final String IN_MULTIPLES_OF_DEPOSIT_TERM = "inMultiplesOfDepositTerm"; + public static final String IN_MULTIPLES_OF_DEPOSIT_TERM_TYPE_ID = "inMultiplesOfDepositTermTypeId"; + public static final String IS_MANDATORY_DEPOSIT = "isMandatoryDeposit"; + public static final String ALLOW_WITHDRAWAL = "allowWithdrawal"; + public static final String ADJUST_ADVANCE_TOWARDS_FUTURE_PAYMENTS = "adjustAdvanceTowardsFuturePayments"; + private final String schemaSql; + + RecurringDepositAccountTemplateMapper(final ClientData client, final GroupGeneralData group) { + super(client, group); + final StringBuilder sqlBuilder = new StringBuilder(400); + sqlBuilder.append(this.selectFieldsSql()); + + sqlBuilder.append(", dptp.pre_closure_penal_applicable as preClosurePenalApplicable, "); + sqlBuilder.append("dptp.pre_closure_penal_interest as preClosurePenalInterest, "); + sqlBuilder.append("dptp.pre_closure_penal_interest_on_enum as preClosurePenalInterestOnId, "); + sqlBuilder.append("dprd.is_mandatory as isMandatoryDeposit, "); + sqlBuilder.append("dprd.allow_withdrawal as allowWithdrawal, "); + sqlBuilder.append("dprd.adjust_advance_towards_future_payments as adjustAdvanceTowardsFuturePayments, "); + sqlBuilder.append("dptp.min_deposit_term as minDepositTerm, "); + sqlBuilder.append("dptp.max_deposit_term as maxDepositTerm, "); + sqlBuilder.append("dptp.min_deposit_term_type_enum as minDepositTermTypeId, "); + sqlBuilder.append("dptp.max_deposit_term_type_enum as maxDepositTermTypeId, "); + sqlBuilder.append("dptp.in_multiples_of_deposit_term as inMultiplesOfDepositTerm, "); + sqlBuilder.append("dptp.in_multiples_of_deposit_term_type_enum as inMultiplesOfDepositTermTypeId, "); + sqlBuilder.append("sa.account_type as accountType, sa.customer_segment as customerSegment, "); + sqlBuilder.append("sa.product_code as productCode, "); + sqlBuilder.append("sa.effective_date as effectiveDate, sa.effective_to as effectiveTo, sa.end_date as endDate, "); + sqlBuilder.append("sa.introducer_required as introducerRequired, sa.share_linking_enabled as shareLinkingEnabled, "); + sqlBuilder.append("sa.nomination_facility as nominationFacility, sa.dormancy_alert as dormancyAlert, "); + sqlBuilder.append("sa.is_dormancy_tracking_active as isDormancyTrackingActive, "); + sqlBuilder.append("sa.days_to_inactive as daysToInactive, "); + sqlBuilder.append("sa.days_to_dormancy as daysToDormancy, "); + sqlBuilder.append("sa.days_to_escheat as daysToEscheat, "); + sqlBuilder.append("sa.interest_calculate_selector as interestCalculateSelector, "); + sqlBuilder.append("icv.code_value as interestCalculationTypeName, "); + sqlBuilder.append("sa.average_period as averagePeriod, "); + sqlBuilder.append("sa.auto_post as autoPost, sa.frequency_fix_day as frequencyFixDay, "); + sqlBuilder.append("sa.last_interest_date as lastInterestDate, sa.enable_lockin_period as enableLockinPeriod, "); + sqlBuilder.append("sa.balance_frequency as balanceFrequency, "); + sqlBuilder.append("sa.account_closed_zero_balance_in_day_closing as accountClosedZeroBalanceInDayClosing, "); + sqlBuilder.append("sa.balance_swap_option as balanceSwapOption, sa.msme as msme, "); + sqlBuilder.append("sa.auto_debit_facility as autoDebitFacility, sa.loan_facility as loanFacility, "); + sqlBuilder.append("sa.premature_withdrawal_option as prematureWithdrawalOption, sa.tenure_flexibility as tenureFlexibility, "); + sqlBuilder.append("sa.lien_marked as lienMarked, sa.maturity_instructions as maturityInstructions, "); + sqlBuilder.append("sa.credit_maturity_proceeds as creditMaturityProceeds, "); + sqlBuilder.append("sa.penalty_grace_period_missed_installments as penaltyGracePeriodMissedInstallments, "); + sqlBuilder.append("sa.panel_request_applicable as panelRequestApplicable, "); + sqlBuilder.append("sa.no_penalty_nominee_heir_closure as noPenaltyNomineeHeirClosure, "); + sqlBuilder.append("sa.deceased_settlement as deceasedSettlement, "); + sqlBuilder.append("sa.allow_debit_txn_after_due_date as allowDebitTxnAfterDueDate "); + + sqlBuilder.append(this.selectTablesSql()); + sqlBuilder.append("left join m_deposit_product_recurring_detail dprd on sa.id = dprd.savings_product_id "); + sqlBuilder.append("left join m_code_value icv on icv.id = sa.interest_calculate_selector "); + + this.schemaSql = sqlBuilder.toString(); + } + + @Override + public String schema() { + return this.schemaSql; + } + + @Override + public RecurringDepositAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { + + final DepositAccountData depositAccountData = super.mapRow(rs); + + final boolean preClosurePenalApplicable = rs.getBoolean(PRE_CLOSURE_PENAL_APPLICABLE); + final BigDecimal preClosurePenalInterest = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, PRE_CLOSURE_PENAL_INTEREST); + final Integer preClosurePenalInterestOnTypeId = JdbcSupport.getInteger(rs, PRE_CLOSURE_PENAL_INTEREST_ON_ID); + final EnumOptionData preClosurePenalInterestOnType = (preClosurePenalInterestOnTypeId == null) ? null + : SavingsEnumerations.preClosurePenaltyInterestOnType(preClosurePenalInterestOnTypeId); + + final Integer minDepositTerm = JdbcSupport.getInteger(rs, MIN_DEPOSIT_TERM); + final Integer maxDepositTerm = JdbcSupport.getInteger(rs, MAX_DEPOSIT_TERM); + final Integer minDepositTermTypeId = JdbcSupport.getInteger(rs, MIN_DEPOSIT_TERM_TYPE_ID); + final EnumOptionData minDepositTermType = (minDepositTermTypeId == null) ? null + : SavingsEnumerations.depositTermFrequencyType(minDepositTermTypeId); + final Integer maxDepositTermTypeId = JdbcSupport.getInteger(rs, MAX_DEPOSIT_TERM_TYPE_ID); + final EnumOptionData maxDepositTermType = (maxDepositTermTypeId == null) ? null + : SavingsEnumerations.depositTermFrequencyType(maxDepositTermTypeId); + final Integer inMultiplesOfDepositTerm = JdbcSupport.getInteger(rs, IN_MULTIPLES_OF_DEPOSIT_TERM); + final Integer inMultiplesOfDepositTermTypeId = JdbcSupport.getInteger(rs, IN_MULTIPLES_OF_DEPOSIT_TERM_TYPE_ID); + final EnumOptionData inMultiplesOfDepositTermType = (inMultiplesOfDepositTermTypeId == null) ? null + : SavingsEnumerations.depositTermFrequencyType(inMultiplesOfDepositTermTypeId); + final boolean isMandatoryDeposit = rs.getBoolean(IS_MANDATORY_DEPOSIT); + final boolean allowWithdrawal = rs.getBoolean(ALLOW_WITHDRAWAL); + final boolean adjustAdvanceTowardsFuturePayments = rs.getBoolean(ADJUST_ADVANCE_TOWARDS_FUTURE_PAYMENTS); + final boolean isCalendarInherited = false; + + final BigDecimal depositAmount = null; + final BigDecimal maturityAmount = null; + final LocalDate maturityDate = null; + final Integer depositPeriod = null; + final EnumOptionData depositPeriodFrequencyType = null; + final LocalDate expectedFirstDepositOnDate = null; + final BigDecimal mandatoryRecommendedDepositAmount = null; + final EnumOptionData onAccountClosureType = null; + final BigDecimal totalOverdueAmount = null; + final Integer noOfOverdueInstallments = null; + + final RecurringDepositAccountData result = RecurringDepositAccountData.instance(depositAccountData, + preClosurePenalApplicable, preClosurePenalInterest, + preClosurePenalInterestOnType, minDepositTerm, maxDepositTerm, minDepositTermType, maxDepositTermType, + inMultiplesOfDepositTerm, inMultiplesOfDepositTermType, depositAmount, maturityAmount, maturityDate, depositPeriod, + depositPeriodFrequencyType, mandatoryRecommendedDepositAmount, onAccountClosureType, expectedFirstDepositOnDate, + totalOverdueAmount, noOfOverdueInstallments, isMandatoryDeposit, allowWithdrawal, adjustAdvanceTowardsFuturePayments, + isCalendarInherited); + + // CBS product fields + result.setCbsProductFields( + rs.getString("accountType"), + rs.getString("customerSegment"), + rs.getString("productCode"), + JdbcSupport.getLocalDate(rs, "effectiveDate"), + JdbcSupport.getLocalDate(rs, "effectiveTo"), + JdbcSupport.getLocalDate(rs, "endDate"), + rs.getBoolean("introducerRequired"), + rs.getBoolean("shareLinkingEnabled"), + rs.getBoolean("nominationFacility"), + rs.getBoolean("dormancyAlert"), + rs.getBoolean("isDormancyTrackingActive"), + JdbcSupport.getInteger(rs, "daysToInactive"), + JdbcSupport.getInteger(rs, "daysToDormancy"), + JdbcSupport.getInteger(rs, "daysToEscheat"), + JdbcSupport.getInteger(rs, "interestCalculateSelector"), + rs.getString("interestCalculationTypeName"), + rs.getString("averagePeriod"), + rs.getBoolean("autoPost"), + JdbcSupport.getInteger(rs, "frequencyFixDay"), + JdbcSupport.getLocalDate(rs, "lastInterestDate"), + rs.getBoolean("enableLockinPeriod"), + rs.getString("balanceFrequency"), + rs.getBoolean("accountClosedZeroBalanceInDayClosing"), + rs.getBoolean("balanceSwapOption"), + rs.getBoolean("msme"), + rs.getBoolean("autoDebitFacility"), + rs.getBoolean("loanFacility"), + rs.getBoolean("prematureWithdrawalOption"), + rs.getBoolean("tenureFlexibility"), + rs.getBoolean("lienMarked"), + rs.getBoolean("maturityInstructions"), + rs.getBoolean("creditMaturityProceeds"), + rs.getBoolean("penaltyGracePeriodMissedInstallments"), + rs.getBoolean("panelRequestApplicable"), + rs.getBoolean("noPenaltyNomineeHeirClosure"), + rs.getBoolean("deceasedSettlement"), + rs.getBoolean("allowDebitTxnAfterDueDate") + ); + + return result; + } + } + + private static final class DepositAccountForMaturityMapper implements RowMapper { + + public static final String ID = "id"; + public static final String ACCOUNT_NUMBER = "accountNumber"; + public static final String DEPOSIT_TYPE_ID = "depositTypeId"; + + public String schema() { + LocalDate today = DateUtils.getBusinessLocalDate(); + String formattedToday = DATE_TIME_FORMATTER.format(today); + final StringBuilder sqlBuilder = new StringBuilder(200); + sqlBuilder.append("da.id as id, "); + sqlBuilder.append("da.account_no as accountNumber, "); + sqlBuilder.append("da.deposit_type_enum as depositTypeId "); + sqlBuilder.append("FROM m_savings_account da "); + sqlBuilder.append("inner join m_deposit_account_term_and_preclosure dat on dat.savings_account_id = da.id "); + sqlBuilder.append("and dat.maturity_date is not null and dat.maturity_date <= '" + formattedToday + "' "); + + return sqlBuilder.toString(); + } + + @Override + public DepositAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { + + final Long id = rs.getLong(ID); + final String name = rs.getString(ACCOUNT_NUMBER); + final Integer depositTypeId = JdbcSupport.getInteger(rs, DEPOSIT_TYPE_ID); + final EnumOptionData depositType = (depositTypeId == null) ? null : SavingsEnumerations.depositType(depositTypeId); + + return DepositAccountData.lookup(id, name, depositType); + } + } + + private static final class RecurringAccountDepositTransactionTemplateMapper implements RowMapper { + + public static final String ID = "id"; + public static final String ACCOUNT_NO = "accountNo"; + public static final String DUEAMOUNT = "dueamount"; + public static final String OUTSTANDING_CHARGE_AMOUNT = "outstandingChargeAmount"; + public static final String DUEDATE = "duedate"; + public static final String CURRENCY_CODE = "currencyCode"; + public static final String CURRENCY_NAME = "currencyName"; + public static final String CURRENCY_NAME_CODE = "currencyNameCode"; + public static final String CURRENCY_DISPLAY_SYMBOL = "currencyDisplaySymbol"; + public static final String CURRENCY_DIGITS = "currencyDigits"; + public static final String IN_MULTIPLES_OF = "inMultiplesOf"; + public static final String RUNNING_BALANCE = "runningBalance"; + private final String schemaSql; + + RecurringAccountDepositTransactionTemplateMapper() { + final StringBuilder sqlBuilder = new StringBuilder(400); + sqlBuilder.append("sa.id as id, sa.account_no as accountNo, "); + sqlBuilder.append( + "sa.currency_code as currencyCode, sa.currency_digits as currencyDigits, 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("sa.account_balance_derived as runningBalance, "); + sqlBuilder.append( + "mss.duedate as duedate, (mss.deposit_amount - coalesce(mss.deposit_amount_completed_derived,0)) as dueamount, "); + sqlBuilder.append("coalesce(sac.amount_outstanding_derived,0.0) AS outstandingChargeAmount "); + sqlBuilder.append("from m_savings_account sa "); + sqlBuilder.append("join m_mandatory_savings_schedule mss on mss.savings_account_id=sa.id and mss.completed_derived = false "); + sqlBuilder.append("join m_currency curr on curr.code = sa.currency_code "); + sqlBuilder.append("LEFT JOIN(SELECT s.savings_account_id AS savings_account_id "); + sqlBuilder.append(",SUM(COALESCE(s.amount_outstanding_derived,0.0)) AS amount_outstanding_derived "); + sqlBuilder.append("FROM m_savings_account_charge s "); + sqlBuilder.append("JOIN m_charge c ON c.id = s.charge_id AND c.charge_time_enum = 3 "); + sqlBuilder.append("WHERE s.savings_account_id = ? "); + sqlBuilder.append("AND s.is_active = TRUE GROUP BY s.savings_account_id)sac ON sac.savings_account_id = sa.id "); + + this.schemaSql = sqlBuilder.toString(); + } + + public String schema() { + return this.schemaSql; + } + + @Override + public SavingsAccountTransactionData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { + + final Long savingsId = rs.getLong(ID); + final String accountNo = rs.getString(ACCOUNT_NO); + final BigDecimal dueamount = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, DUEAMOUNT); + final BigDecimal outstandingChargeAmount = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, OUTSTANDING_CHARGE_AMOUNT); + final LocalDate duedate = JdbcSupport.getLocalDate(rs, DUEDATE); + final String currencyCode = rs.getString(CURRENCY_CODE); + final String currencyName = rs.getString(CURRENCY_NAME); + final String currencyNameCode = rs.getString(CURRENCY_NAME_CODE); + final String currencyDisplaySymbol = rs.getString(CURRENCY_DISPLAY_SYMBOL); + final Integer currencyDigits = JdbcSupport.getInteger(rs, CURRENCY_DIGITS); + final Integer inMultiplesOf = JdbcSupport.getInteger(rs, IN_MULTIPLES_OF); + final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMultiplesOf, currencyDisplaySymbol, + currencyNameCode); + final SavingsAccountTransactionEnumData transactionType = SavingsEnumerations + .transactionType(SavingsAccountTransactionType.DEPOSIT.getValue()); + final PaymentDetailData paymentDetailData = null; + final AccountTransferData transfer = null; + final BigDecimal runningBalance = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, RUNNING_BALANCE); + final boolean postInterestAsOn = false; + final String submittedByUsername = null; + final String note = null; + final LocalDate submittedOnDate = DateUtils.getBusinessLocalDate(); + return SavingsAccountTransactionData.create(savingsId, transactionType, paymentDetailData, savingsId, accountNo, duedate, + currency, dueamount, outstandingChargeAmount, runningBalance, false, transfer, postInterestAsOn, submittedByUsername, + note, submittedOnDate); + } + } + + private static class AccountTransferMapper implements RowMapper { + + public static final String FROM_ACC = "fromAcc"; + public static final String TO_ACC = "toAcc"; + public static final String AMOUNT = "amount"; + public static final String TRANSACTION_DATE = "transactionDate"; + public static final String TRANSFER_INTEREST_TO_SAVINGS = "transfer interest to savings"; + private final String schemaSql; + + AccountTransferMapper() { + final StringBuilder sqlBuilder = new StringBuilder(400); + sqlBuilder.append( + "sa.id as fromAcc ,aa.linked_savings_account_id as toAcc,st.amount as amount, st.transaction_date as transactionDate ") + .append(" from m_deposit_account_term_and_preclosure da ") + .append(" inner join m_savings_account sa on da.savings_account_id = sa.id") + .append(" inner join m_savings_account_transaction st on st.savings_account_id = sa.id") + .append(" inner join m_portfolio_account_associations aa on aa.savings_account_id=sa.id"); + schemaSql = sqlBuilder.toString(); + } + + public String schema() { + return this.schemaSql; + } + + @Override + public AccountTransferDTO mapRow(ResultSet rs, @SuppressWarnings("unused") int rowNum) throws SQLException { + final Long fromAccountId = rs.getLong(FROM_ACC); + final Long toAccountId = rs.getLong(TO_ACC); + final BigDecimal transactionAmount = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, AMOUNT); + final boolean isRegularTransaction = false; + final boolean isExceptionForBalanceCheck = false; + final LocalDate transactionDate = JdbcSupport.getLocalDate(rs, TRANSACTION_DATE); + return new AccountTransferDTO(transactionDate, transactionAmount, PortfolioAccountType.SAVINGS, PortfolioAccountType.SAVINGS, + fromAccountId, toAccountId, TRANSFER_INTEREST_TO_SAVINGS, null, null, null, null, null, null, null, + AccountTransferType.INTEREST_TRANSFER.getValue(), null, null, ExternalId.empty(), null, null, null, + isRegularTransaction, isExceptionForBalanceCheck); + } + + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java index 3080f530a..f4453fbf2 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java @@ -1,1533 +1,1545 @@ -/** - * 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.infrastructure.core.domain.AuditableFieldsConstants.CREATED_BY_DB_FIELD; -import static org.apache.fineract.infrastructure.core.domain.AuditableFieldsConstants.CREATED_DATE_DB_FIELD; - -import java.math.BigDecimal; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import org.apache.commons.lang3.StringUtils; -import org.apache.fineract.accounting.common.AccountingRuleType; -import org.apache.fineract.accounting.glaccount.data.GLAccountData; -import org.apache.fineract.infrastructure.core.data.EnumOptionData; -import org.apache.fineract.infrastructure.core.domain.ExternalId; -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.service.PlatformSecurityContext; -import org.apache.fineract.infrastructure.security.utils.ColumnValidator; -import org.apache.fineract.organisation.monetary.data.CurrencyData; -import org.apache.fineract.portfolio.account.data.AccountTransferData; -import org.apache.fineract.portfolio.client.data.ClientData; -import org.apache.fineract.portfolio.group.data.GroupGeneralData; -import org.apache.fineract.portfolio.paymentdetail.data.PaymentDetailData; -import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData; -import org.apache.fineract.portfolio.savings.DepositAccountType; -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.SavingsAccountApplicationTimelineData; -import org.apache.fineract.portfolio.savings.data.SavingsAccountChargeData; -import org.apache.fineract.portfolio.savings.data.SavingsAccountData; -import org.apache.fineract.portfolio.savings.data.SavingsAccountStatusEnumData; -import org.apache.fineract.portfolio.savings.data.SavingsAccountSubStatusEnumData; -import org.apache.fineract.portfolio.savings.data.SavingsAccountSummaryData; -import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData; -import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionEnumData; -import org.apache.fineract.portfolio.savings.data.SavingsAccrualData; -import org.apache.fineract.portfolio.savings.data.SavingsProductData; -import org.apache.fineract.portfolio.savings.domain.SavingsAccount; -import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler; -import org.apache.fineract.portfolio.savings.domain.SavingsAccountChargesPaidByData; -import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper; -import org.apache.fineract.portfolio.savings.domain.SavingsAccountStatusType; -import org.apache.fineract.portfolio.savings.domain.SavingsAccountSubStatusEnum; -import org.apache.fineract.portfolio.savings.exception.SavingsAccountNotFoundException; -import org.apache.fineract.portfolio.tax.data.TaxComponentData; -import org.apache.fineract.portfolio.tax.data.TaxDetailsData; -import org.apache.fineract.portfolio.tax.data.TaxGroupData; -import org.apache.fineract.useradministration.domain.AppUser; -import org.springframework.dao.DataAccessException; -import org.springframework.dao.EmptyResultDataAccessException; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.ResultSetExtractor; -import org.springframework.jdbc.core.RowMapper; - -public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountReadPlatformService { - - private final PlatformSecurityContext context; - private final JdbcTemplate jdbcTemplate; - private final DatabaseSpecificSQLGenerator sqlGenerator; - - // mappers - private final SavingsAccountTransactionTemplateMapper transactionTemplateMapper; - protected SavingsAccountTransactionsMapper transactionsMapper; - private final SavingsAccountTransactionsForBatchMapper savingsAccountTransactionsForBatchMapper; - private final SavingAccountMapper savingAccountMapper; - private final SavingAccountMapperForInterestPosting savingAccountMapperForInterestPosting; - // private final SavingsAccountAnnualFeeMapper annualFeeMapper; - - // pagination - private final PaginationHelper paginationHelper; - - private final ColumnValidator columnValidator; - private final SavingsAccountAssembler savingAccountAssembler; - - private final SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper; - - public SavingsAccountReadPlatformServiceImpl(final PlatformSecurityContext context, final JdbcTemplate jdbcTemplate, - final SavingsAccountAssembler savingAccountAssembler, PaginationHelper paginationHelper, ColumnValidator columnValidator, - DatabaseSpecificSQLGenerator sqlGenerator, SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper) { - this.context = context; - this.jdbcTemplate = jdbcTemplate; - this.sqlGenerator = sqlGenerator; - this.savingsAccountRepositoryWrapper = savingsAccountRepositoryWrapper; - this.transactionTemplateMapper = new SavingsAccountTransactionTemplateMapper(); - this.transactionsMapper = new SavingsAccountTransactionsMapper(); - this.savingsAccountTransactionsForBatchMapper = new SavingsAccountTransactionsForBatchMapper(); - this.savingAccountMapper = new SavingAccountMapper(); - this.columnValidator = columnValidator; - this.paginationHelper = paginationHelper; - this.savingAccountMapperForInterestPosting = new SavingAccountMapperForInterestPosting(); - this.savingAccountAssembler = savingAccountAssembler; - } - - @Override - public Collection retrieveAllForLookup(final Long clientId) { - - final StringBuilder sqlBuilder = new StringBuilder("select " + this.savingAccountMapper.schema()); - sqlBuilder.append(" where sa.client_id = ? and sa.status_enum = 300 "); - - final Object[] queryParameters = new Object[] { clientId }; - return this.jdbcTemplate.query(sqlBuilder.toString(), this.savingAccountMapper, queryParameters); - } - - @Override - public Collection retrieveActiveForLookup(final Long clientId, DepositAccountType depositAccountType) { - - final StringBuilder sqlBuilder = new StringBuilder("select " + this.savingAccountMapper.schema()); - sqlBuilder.append(" where sa.client_id = ? and sa.status_enum = 300 and sa.deposit_type_enum = ? "); - - final Object[] queryParameters = new Object[] { clientId, depositAccountType.getValue() }; - return this.jdbcTemplate.query(sqlBuilder.toString(), this.savingAccountMapper, queryParameters); - } - - @Override - public Collection retrieveActiveForLookup(final Long clientId, DepositAccountType depositAccountType, - String currencyCode) { - final StringBuilder sqlBuilder = new StringBuilder("select " + this.savingAccountMapper.schema()); - sqlBuilder.append(" where sa.client_id = ? and sa.status_enum = 300 and sa.deposit_type_enum = ? and sa.currency_code = ? "); - - final Object[] queryParameters = new Object[] { clientId, depositAccountType.getValue(), currencyCode }; - return this.jdbcTemplate.query(sqlBuilder.toString(), this.savingAccountMapper, queryParameters); - } - - @Override - public Page retrieveAll(final SearchParameters searchParameters) { - - final AppUser currentUser = this.context.authenticatedUser(); - final String hierarchy = currentUser.getOffice().getHierarchy(); - final String hierarchySearchString = hierarchy + "%"; - - final StringBuilder sqlBuilder = new StringBuilder(200); - sqlBuilder.append("select " + sqlGenerator.calcFoundRows() + " "); - sqlBuilder.append(this.savingAccountMapper.schema()); - - sqlBuilder.append(" join m_office o on o.id = c.office_id"); - sqlBuilder.append(" where o.hierarchy like ?"); - - final Object[] objectArray = new Object[3]; - objectArray[0] = hierarchySearchString; - int arrayPos = 1; - if (searchParameters != null) { - - if (StringUtils.isNotBlank(searchParameters.getStatus())) { - sqlBuilder.append(" and sa.status_enum = ?"); - objectArray[arrayPos] = Integer.parseInt(searchParameters.getStatus()); - arrayPos = arrayPos + 1; - } - - if (StringUtils.isNotBlank(searchParameters.getExternalId())) { - sqlBuilder.append(" and sa.external_id = ?"); - objectArray[arrayPos] = searchParameters.getExternalId(); - arrayPos = arrayPos + 1; - } - if (searchParameters.getOfficeId() != null) { - sqlBuilder.append(" and c.office_id = ?"); - objectArray[arrayPos++] = searchParameters.getOfficeId(); - } - 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 = Arrays.copyOf(objectArray, arrayPos); - return this.paginationHelper.fetchPage(this.jdbcTemplate, sqlBuilder.toString(), finalObjectArray, this.savingAccountMapper); - } - - @Override - public SavingsAccountData retrieveOne(final Long accountId) { - - try { - final String sql = "select " + this.savingAccountMapper.schema() + " where sa.id = ?"; - - final SavingsAccountData accountData = this.jdbcTemplate.queryForObject(sql, this.savingAccountMapper, - new Object[] { accountId }); // NOSONAR - - // Load joint holders - if (accountData != null) { - final String jointSql = "SELECT jh.id as id, jh.client_id as clientId, jh.cif_number as cifNumber, " - + "jh.display_name as displayName, jh.share_percentage as sharePercentage, " - + "jh.signing_authority as signingAuthority, jh.role as role, jh.is_primary as isPrimary " - + "FROM m_savings_account_joint_holders jh WHERE jh.savings_account_id = ?"; - final List> jointHolders = this.jdbcTemplate.queryForList(jointSql, accountId); - if (!jointHolders.isEmpty()) { - accountData.setJointHolders(jointHolders); - } - } - - return accountData; - } catch (final EmptyResultDataAccessException e) { - throw new SavingsAccountNotFoundException(accountId, e); - } - } - - @Override - public List retrieveAllTransactionData(final List refNo) throws DataAccessException { - String inSql = String.join(",", Collections.nCopies(refNo.size(), "?")); - String sql = "select " + this.savingsAccountTransactionsForBatchMapper.schema() + " where tr.ref_no in (%s)"; - Object[] params = new Object[refNo.size()]; - int i = 0; - for (String element : refNo) { - params[i] = element; - i++; - } - return this.jdbcTemplate.query(String.format(sql, inSql), this.savingsAccountTransactionsForBatchMapper, params); - } - - @Override - public List retrieveAllSavingsDataForInterestPosting(final boolean backdatedTxnsAllowedTill, final int pageSize, - final Integer status, final Long maxSavingsId) { - LocalDate yesterday = DateUtils.getBusinessLocalDate().minusDays(1); - String sql = "select " + this.savingAccountMapperForInterestPosting.schema() - + "join (select a.id from m_savings_account a where a.id > ? and a.status_enum = ? limit ?) b on b.id = sa.id "; - if (backdatedTxnsAllowedTill) { - sql = sql - + "where (CASE WHEN sa.interest_posted_till_date is not null THEN tr.transaction_date >= sa.interest_posted_till_date ELSE tr.transaction_date >= sa.activatedon_date END) "; - } - - sql = sql + " and (sa.interest_posted_till_date is null or sa.interest_posted_till_date <= ? ) "; - // #audit backward compatibility - sql = sql + " order by sa.id, tr.transaction_date, tr." + CREATED_DATE_DB_FIELD + ", tr.created_date, tr.id"; - - List savingsAccountDataList = this.jdbcTemplate.query(sql, this.savingAccountMapperForInterestPosting, // NOSONAR - new Object[] { maxSavingsId, status, pageSize, yesterday }); - for (SavingsAccountData savingsAccountData : savingsAccountDataList) { - this.savingAccountAssembler.assembleSavings(savingsAccountData); - } - return savingsAccountDataList; - } - - private static final class SavingAccountMapperForInterestPosting implements ResultSetExtractor> { - - private final String schemaSql; - - SavingAccountMapperForInterestPosting() { - final StringBuilder sqlBuilder = new StringBuilder(400); - sqlBuilder.append("sa.id as id, sa.account_no as accountNo, sa.external_id as externalId, "); - sqlBuilder.append("sa.deposit_type_enum as depositType, "); - sqlBuilder.append("c.id as clientId, c.office_id as clientOfficeId, "); - sqlBuilder.append("g.id as groupId, g.office_id as groupOfficeId, "); - sqlBuilder.append("sa.status_enum as statusEnum, "); - sqlBuilder.append("sa.sub_status_enum as subStatusEnum, "); - sqlBuilder.append("sa.submittedon_date as submittedOnDate,"); - sqlBuilder.append("sa.rejectedon_date as rejectedOnDate,"); - sqlBuilder.append("sa.withdrawnon_date as withdrawnOnDate,"); - sqlBuilder.append("sa.approvedon_date as approvedOnDate,"); - sqlBuilder.append("sa.activatedon_date as activatedOnDate,"); - sqlBuilder.append("sa.closedon_date as closedOnDate,"); - sqlBuilder.append( - "sa.currency_code as currencyCode, sa.currency_digits as currencyDigits, sa.currency_multiplesof as inMultiplesOf, "); - sqlBuilder.append("sa.nominal_annual_interest_rate as nominalAnnualInterestRate, "); - sqlBuilder.append("sa.interest_compounding_period_enum as interestCompoundingPeriodType, "); - sqlBuilder.append("sa.interest_posting_period_enum as interestPostingPeriodType, "); - sqlBuilder.append("sa.interest_calculation_type_enum as interestCalculationType, "); - sqlBuilder.append("sa.interest_calculation_days_in_year_type_enum as interestCalculationDaysInYearType, "); - sqlBuilder.append("sa.min_required_opening_balance as minRequiredOpeningBalance, "); - sqlBuilder.append("sa.lockin_period_frequency as lockinPeriodFrequency,"); - sqlBuilder.append("sa.lockin_period_frequency_enum as lockinPeriodFrequencyType, "); - sqlBuilder.append("sa.withdrawal_fee_for_transfer as withdrawalFeeForTransfers, "); - sqlBuilder.append("sa.allow_overdraft as allowOverdraft, "); - sqlBuilder.append("sa.overdraft_limit as overdraftLimit, "); - sqlBuilder.append("sa.nominal_annual_interest_rate_overdraft as nominalAnnualInterestRateOverdraft, "); - sqlBuilder.append("sa.min_overdraft_for_interest_calculation as minOverdraftForInterestCalculation, "); - sqlBuilder.append("sa.total_deposits_derived as totalDeposits, "); - sqlBuilder.append("sa.total_withdrawals_derived as totalWithdrawals, "); - sqlBuilder.append("sa.total_withdrawal_fees_derived as totalWithdrawalFees, "); - sqlBuilder.append("sa.total_annual_fees_derived as totalAnnualFees, "); - sqlBuilder.append("sa.total_interest_earned_derived as totalInterestEarned, "); - sqlBuilder.append("sa.total_interest_posted_derived as totalInterestPosted, "); - sqlBuilder.append("sa.total_overdraft_interest_derived as totalOverdraftInterestDerived, "); - sqlBuilder.append("sa.account_balance_derived as accountBalance, "); - sqlBuilder.append("sa.total_fees_charge_derived as totalFeeCharge, "); - sqlBuilder.append("sa.total_penalty_charge_derived as totalPenaltyCharge, "); - sqlBuilder.append("sa.min_balance_for_interest_calculation as minBalanceForInterestCalculation,"); - sqlBuilder.append("sa.min_required_balance as minRequiredBalance, "); - sqlBuilder.append("sa.enforce_min_required_balance as enforceMinRequiredBalance, "); - sqlBuilder.append("sa.max_allowed_lien_limit as maxAllowedLienLimit, "); - sqlBuilder.append("sa.is_lien_allowed as lienAllowed, "); - sqlBuilder.append("sa.on_hold_funds_derived as onHoldFunds, "); - sqlBuilder.append("sa.withhold_tax as withHoldTax, "); - sqlBuilder.append("sa.total_withhold_tax_derived as totalWithholdTax, "); - sqlBuilder.append("sa.last_interest_calculation_date as lastInterestCalculationDate, "); - sqlBuilder.append("sa.total_savings_amount_on_hold as onHoldAmount, "); - sqlBuilder.append("sa.interest_posted_till_date as interestPostedTillDate, "); - sqlBuilder.append("tg.id as taxGroupId, "); - sqlBuilder.append("(select COALESCE(max(sat.transaction_date),sa.activatedon_date) "); - sqlBuilder.append("from m_savings_account_transaction as sat "); - sqlBuilder.append("where sat.is_reversed = false and sat.is_reversal = false "); - sqlBuilder.append("and sat.transaction_type_enum in (1,2) "); - sqlBuilder.append("and sat.savings_account_id = sa.id) as lastActiveTransactionDate, "); - sqlBuilder.append("sp.id as productId, "); - sqlBuilder.append("sp.is_dormancy_tracking_active as isDormancyTrackingActive, "); - sqlBuilder.append("sp.days_to_inactive as daysToInactive, "); - sqlBuilder.append("sp.days_to_dormancy as daysToDormancy, "); - sqlBuilder.append("sp.days_to_escheat as daysToEscheat, "); - sqlBuilder.append("sp.accounting_type as accountingType, "); - sqlBuilder.append("tr.id as transactionId, tr.transaction_type_enum as transactionType, "); - sqlBuilder.append("tr.transaction_date as transactionDate, tr.amount as transactionAmount,"); - sqlBuilder.append("tr.submitted_on_date as transSubmittedOnDate,tr.cumulative_balance_derived as cumulativeBalance,"); - sqlBuilder.append("tr.running_balance_derived as runningBalance, tr.is_reversed as reversed,"); - sqlBuilder.append("tr.balance_end_date_derived as balanceEndDate, tr.overdraft_amount_derived as overdraftAmount,"); - sqlBuilder.append("tr.is_manual as manualTransaction,tr.office_id as officeId, "); - sqlBuilder.append("pd.payment_type_id as paymentType,pd.account_number as accountNumber,pd.check_number as checkNumber, "); - sqlBuilder.append("pd.receipt_number as receiptNumber, pd.bank_number as bankNumber,pd.routing_code as routingCode, "); - sqlBuilder.append("pt.value as paymentTypeName, "); - sqlBuilder.append("msacpb.amount as paidByAmount, msacpb.id as chargesPaidById, "); - sqlBuilder.append( - "msac.id as chargeId, msac.amount as chargeAmount, msac.charge_time_enum as chargeTimeType, msac.is_penalty as isPenaltyCharge, "); - sqlBuilder.append("txd.id as taxDetailsId, txd.amount as taxAmount, "); - sqlBuilder.append("apm.gl_account_id as glAccountIdForInterestOnSavings, apm1.gl_account_id as glAccountIdForSavingsControl, "); - sqlBuilder.append( - "apm2.gl_account_id as glAccountIdForInterestReceivable,apm3.gl_account_id as glAccountIdForOverdraftPorfolio, "); - sqlBuilder.append("apm4.gl_account_id as glAccountIdForInterestPayable, "); - sqlBuilder.append( - "mtc.id as taxComponentId, mtc.debit_account_id as debitAccountId, mtc.credit_account_id as creditAccountId, mtc.percentage as taxPercentage "); - 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("join m_savings_account_transaction tr on sa.id = tr.savings_account_id "); - sqlBuilder.append("left join m_payment_detail pd on pd.id = tr.payment_detail_id "); - sqlBuilder.append("left join m_payment_type pt on pt.id = pd.payment_type_id "); - sqlBuilder.append("left join m_savings_account_charge_paid_by msacpb on msacpb.savings_account_transaction_id = tr.id "); - sqlBuilder.append("left join m_savings_account_charge msac on msac.id = msacpb.savings_account_charge_id "); - 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_tax_group tg on tg.id = sa.tax_group_id "); - sqlBuilder.append("left join m_savings_account_transaction_tax_details txd on txd.savings_transaction_id = tr.id "); - sqlBuilder.append("left join m_tax_component mtc on mtc.id = txd.tax_component_id "); - sqlBuilder.append( - "left join acc_product_mapping apm on apm.product_type = 2 and apm.product_id = sp.id and apm.financial_account_type=3 "); - sqlBuilder.append( - "left join acc_product_mapping apm1 on apm1.product_type = 2 and apm1.product_id = sp.id and apm1.financial_account_type=2 "); - sqlBuilder.append("left join acc_product_mapping apm2 on apm2.product_id = sp.id and apm2.financial_account_type=18 "); - sqlBuilder.append("left join acc_product_mapping apm3 on apm3.product_id = sp.id and apm3.financial_account_type = 11 "); - sqlBuilder.append("left join acc_product_mapping apm4 on apm4.product_id = sp.id and apm4.financial_account_type = 17 "); - - this.schemaSql = sqlBuilder.toString(); - } - - public String schema() { - return this.schemaSql; - } - - @Override - public List extractData(final ResultSet rs) throws SQLException { - - List savingsAccountDataList = new ArrayList<>(); - HashMap savingsMap = new HashMap<>(); - String currencyCode = null; - Integer currencyDigits = null; - Integer inMultiplesOf = null; - CurrencyData currency = null; - HashMap transMap = new HashMap<>(); - HashMap taxDetails = new HashMap<>(); - HashMap chargeDetails = new HashMap<>(); - SavingsAccountTransactionData savingsAccountTransactionData = null; - SavingsAccountData savingsAccountData = null; - int count = 0; - - while (rs.next()) { - final Long id = rs.getLong("id"); - final Long transactionId = rs.getLong("transactionId"); - final Long taxDetailId = JdbcSupport.getLongDefaultToNullIfZero(rs, "taxDetailsId"); - final Long taxComponentId = JdbcSupport.getLongDefaultToNullIfZero(rs, "taxComponentId"); - final String accountNo = rs.getString("accountNo"); - final Long chargeId = rs.getLong("chargeId"); - - if (!savingsMap.containsValue(id)) { - if (count > 0) { - savingsAccountDataList.add(savingsAccountData); - } - count++; - savingsMap.put("id", id); - - final String externalId = rs.getString("externalId"); - final Integer depositTypeId = rs.getInt("depositType"); - final EnumOptionData depositType = SavingsEnumerations.depositType(depositTypeId); - final Long groupId = JdbcSupport.getLong(rs, "groupId"); - final Long groupOfficeId = JdbcSupport.getLong(rs, "groupOfficeId"); - final GroupGeneralData groupGeneralData = new GroupGeneralData(groupId, groupOfficeId); - - final Long clientId = JdbcSupport.getLong(rs, "clientId"); - final Long clientOfficeId = JdbcSupport.getLong(rs, "clientOfficeId"); - final ClientData clientData = ClientData.createClientForInterestPosting(clientId, clientOfficeId); - - final Long glAccountIdForInterestOnSavings = rs.getLong("glAccountIdForInterestOnSavings"); - final Long glAccountIdForSavingsControl = rs.getLong("glAccountIdForSavingsControl"); - - final Long glAccountIdForOverdraftPorfolio = rs.getLong("glAccountIdForOverdraftPorfolio"); - final Long glAccountIdForInterestReceivable = rs.getLong("glAccountIdForInterestReceivable"); - - final Long glAccountIdForInterestPayable = rs.getLong("glAccountIdForInterestPayable"); - - final Long productId = rs.getLong("productId"); - final Integer accountType = rs.getInt("accountingType"); - final AccountingRuleType accountingRuleType = AccountingRuleType.fromInt(accountType); - final EnumOptionData enumOptionDataForAccounting = new EnumOptionData(accountType.longValue(), - accountingRuleType.getCode(), accountingRuleType.getValue().toString()); - final SavingsProductData savingsProductData = SavingsProductData.createForInterestPosting(productId, - enumOptionDataForAccounting); - - final Integer statusEnum = JdbcSupport.getInteger(rs, "statusEnum"); - final SavingsAccountStatusEnumData status = SavingsEnumerations.status(statusEnum); - final Integer subStatusEnum = JdbcSupport.getInteger(rs, "subStatusEnum"); - final SavingsAccountSubStatusEnumData subStatus = SavingsEnumerations.subStatus(subStatusEnum); - final LocalDate lastActiveTransactionDate = JdbcSupport.getLocalDate(rs, "lastActiveTransactionDate"); - final boolean isDormancyTrackingActive = rs.getBoolean("isDormancyTrackingActive"); - final Integer numDaysToInactive = JdbcSupport.getInteger(rs, "daysToInactive"); - final Integer numDaysToDormancy = JdbcSupport.getInteger(rs, "daysToDormancy"); - final Integer numDaysToEscheat = JdbcSupport.getInteger(rs, "daysToEscheat"); - Integer daysToInactive = null; - Integer daysToDormancy = null; - Integer daysToEscheat = null; - - LocalDate currentDate = DateUtils.getBusinessLocalDate(); - if (isDormancyTrackingActive && statusEnum.equals(SavingsAccountStatusType.ACTIVE.getValue())) { - if (subStatusEnum < SavingsAccountSubStatusEnum.ESCHEAT.getValue()) { - daysToEscheat = DateUtils.getExactDifferenceInDays(currentDate, - lastActiveTransactionDate.plusDays(numDaysToEscheat)); - } - if (subStatusEnum < SavingsAccountSubStatusEnum.DORMANT.getValue()) { - daysToDormancy = DateUtils.getExactDifferenceInDays(currentDate, - lastActiveTransactionDate.plusDays(numDaysToDormancy)); - } - if (subStatusEnum < SavingsAccountSubStatusEnum.INACTIVE.getValue()) { - daysToInactive = DateUtils.getExactDifferenceInDays(currentDate, - lastActiveTransactionDate.plusDays(numDaysToInactive)); - } - } - final LocalDate approvedOnDate = JdbcSupport.getLocalDate(rs, "approvedOnDate"); - final LocalDate withdrawnOnDate = JdbcSupport.getLocalDate(rs, "withdrawnOnDate"); - final LocalDate submittedOnDate = JdbcSupport.getLocalDate(rs, "submittedOnDate"); - final LocalDate activatedOnDate = JdbcSupport.getLocalDate(rs, "activatedOnDate"); - final LocalDate closedOnDate = JdbcSupport.getLocalDate(rs, "closedOnDate"); - final SavingsAccountApplicationTimelineData timeline = new SavingsAccountApplicationTimelineData(submittedOnDate, null, - null, null, null, null, null, null, withdrawnOnDate, null, null, null, approvedOnDate, null, null, null, - activatedOnDate, null, null, null, closedOnDate, null, null, null); - - currencyCode = rs.getString("currencyCode"); - currencyDigits = JdbcSupport.getInteger(rs, "currencyDigits"); - inMultiplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf"); - currency = new CurrencyData(currencyCode, currencyDigits, inMultiplesOf); - - final BigDecimal totalDeposits = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalDeposits"); - final BigDecimal totalWithdrawals = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalWithdrawals"); - final BigDecimal totalWithdrawalFees = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalWithdrawalFees"); - final BigDecimal totalAnnualFees = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalAnnualFees"); - - final BigDecimal totalInterestEarned = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalInterestEarned"); - final BigDecimal totalInterestPosted = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "totalInterestPosted"); - final BigDecimal accountBalance = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "accountBalance"); - final BigDecimal totalFeeCharge = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalFeeCharge"); - final BigDecimal totalPenaltyCharge = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalPenaltyCharge"); - final BigDecimal totalOverdraftInterestDerived = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, - "totalOverdraftInterestDerived"); - final BigDecimal totalWithholdTax = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalWithholdTax"); - final LocalDate interestPostedTillDate = JdbcSupport.getLocalDate(rs, "interestPostedTillDate"); - - final BigDecimal minBalanceForInterestCalculation = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, - "minBalanceForInterestCalculation"); - final BigDecimal onHoldFunds = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "onHoldFunds"); - - final BigDecimal onHoldAmount = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "onHoldAmount"); - - BigDecimal availableBalance = accountBalance; - if (availableBalance != null && onHoldFunds != null) { - - availableBalance = availableBalance.subtract(onHoldFunds); - } - - if (availableBalance != null && onHoldAmount != null) { - - availableBalance = availableBalance.subtract(onHoldAmount); - } - - BigDecimal interestNotPosted = BigDecimal.ZERO; - LocalDate lastInterestCalculationDate = null; - if (totalInterestEarned != null) { - interestNotPosted = totalInterestEarned.subtract(totalInterestPosted).add(totalOverdraftInterestDerived); - lastInterestCalculationDate = JdbcSupport.getLocalDate(rs, "lastInterestCalculationDate"); - } - - final SavingsAccountSummaryData summary = new SavingsAccountSummaryData(currency, totalDeposits, totalWithdrawals, - totalWithdrawalFees, totalAnnualFees, totalInterestEarned, totalInterestPosted, accountBalance, totalFeeCharge, - totalPenaltyCharge, totalOverdraftInterestDerived, totalWithholdTax, interestNotPosted, - lastInterestCalculationDate, availableBalance, interestPostedTillDate); - summary.setPrevInterestPostedTillDate(interestPostedTillDate); - - final boolean withHoldTax = rs.getBoolean("withHoldTax"); - final Long taxGroupId = JdbcSupport.getLongDefaultToNullIfZero(rs, "taxGroupId"); - TaxGroupData taxGroupData = null; - if (taxGroupId != null) { - taxGroupData = TaxGroupData.lookup(taxGroupId, null); - } - - final BigDecimal nominalAnnualInterestRate = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, - "nominalAnnualInterestRate"); - - final EnumOptionData interestCompoundingPeriodType = SavingsEnumerations.compoundingInterestPeriodType( - SavingsCompoundingInterestPeriodType.fromInt(JdbcSupport.getInteger(rs, "interestCompoundingPeriodType"))); - - final EnumOptionData interestPostingPeriodType = SavingsEnumerations.interestPostingPeriodType( - SavingsPostingInterestPeriodType.fromInt(JdbcSupport.getInteger(rs, "interestPostingPeriodType"))); - - final EnumOptionData interestCalculationType = SavingsEnumerations.interestCalculationType( - SavingsInterestCalculationType.fromInt(JdbcSupport.getInteger(rs, "interestCalculationType"))); - - final EnumOptionData interestCalculationDaysInYearType = SavingsEnumerations - .interestCalculationDaysInYearType(SavingsInterestCalculationDaysInYearType - .fromInt(JdbcSupport.getInteger(rs, "interestCalculationDaysInYearType"))); - - final BigDecimal minRequiredOpeningBalance = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, - "minRequiredOpeningBalance"); - - final Integer lockinPeriodFrequency = JdbcSupport.getInteger(rs, "lockinPeriodFrequency"); - EnumOptionData lockinPeriodFrequencyType = null; - final Integer lockinPeriodFrequencyTypeValue = JdbcSupport.getInteger(rs, "lockinPeriodFrequencyType"); - if (lockinPeriodFrequencyTypeValue != null) { - final SavingsPeriodFrequencyType lockinPeriodType = SavingsPeriodFrequencyType - .fromInt(lockinPeriodFrequencyTypeValue); - lockinPeriodFrequencyType = SavingsEnumerations.lockinPeriodFrequencyType(lockinPeriodType); - } - - final boolean withdrawalFeeForTransfers = rs.getBoolean("withdrawalFeeForTransfers"); - - final boolean allowOverdraft = rs.getBoolean("allowOverdraft"); - final BigDecimal overdraftLimit = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "overdraftLimit"); - final BigDecimal nominalAnnualInterestRateOverdraft = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, - "nominalAnnualInterestRateOverdraft"); - final BigDecimal minOverdraftForInterestCalculation = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, - "minOverdraftForInterestCalculation"); - - final BigDecimal minRequiredBalance = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "minRequiredBalance"); - final boolean enforceMinRequiredBalance = rs.getBoolean("enforceMinRequiredBalance"); - final BigDecimal maxAllowedLienLimit = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "maxAllowedLienLimit"); - final boolean lienAllowed = rs.getBoolean("lienAllowed"); - savingsAccountData = SavingsAccountData.instance(id, accountNo, depositType, externalId, null, null, null, null, - productId, null, null, null, status, subStatus, null, timeline, currency, nominalAnnualInterestRate, - interestCompoundingPeriodType, interestPostingPeriodType, interestCalculationType, - interestCalculationDaysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType, - withdrawalFeeForTransfers, summary, allowOverdraft, overdraftLimit, minRequiredBalance, - enforceMinRequiredBalance, maxAllowedLienLimit, lienAllowed, minBalanceForInterestCalculation, onHoldFunds, - nominalAnnualInterestRateOverdraft, minOverdraftForInterestCalculation, withHoldTax, taxGroupData, - lastActiveTransactionDate, isDormancyTrackingActive, daysToInactive, daysToDormancy, daysToEscheat, - onHoldAmount); - - savingsAccountData.setClientData(clientData); - savingsAccountData.setGroupGeneralData(groupGeneralData); - savingsAccountData.setSavingsProduct(savingsProductData); - - savingsAccountData.setGlAccountIdForInterestReceivable(glAccountIdForInterestReceivable); - savingsAccountData.setGlAccountIdForOverdraftPorfolio(glAccountIdForOverdraftPorfolio); - - savingsAccountData.setGlAccountIdForInterestPayable(glAccountIdForInterestPayable); - - savingsAccountData.setGlAccountIdForInterestOnSavings(glAccountIdForInterestOnSavings); - savingsAccountData.setGlAccountIdForSavingsControl(glAccountIdForSavingsControl); - } - - if (!transMap.containsValue(transactionId)) { - - final int transactionTypeInt = JdbcSupport.getInteger(rs, "transactionType"); - final SavingsAccountTransactionEnumData transactionType = SavingsEnumerations.transactionType(transactionTypeInt); - - final LocalDate date = JdbcSupport.getLocalDate(rs, "transactionDate"); - final LocalDate balanceEndDate = JdbcSupport.getLocalDate(rs, "balanceEndDate"); - final LocalDate transSubmittedOnDate = JdbcSupport.getLocalDate(rs, "transSubmittedOnDate"); - final BigDecimal amount = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "transactionAmount"); - final BigDecimal overdraftAmount = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "overdraftAmount"); - final BigDecimal outstandingChargeAmount = null; - final BigDecimal runningBalance = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "runningBalance"); - final boolean reversed = rs.getBoolean("reversed"); - final Long officeId = rs.getLong("officeId"); - final BigDecimal cumulativeBalance = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "cumulativeBalance"); - - final boolean postInterestAsOn = false; - - PaymentDetailData paymentDetailData = null; - if (transactionType.isDepositOrWithdrawal()) { - final Long paymentTypeId = JdbcSupport.getLong(rs, "paymentType"); - if (paymentTypeId != null) { - final String typeName = rs.getString("paymentTypeName"); - final PaymentTypeData paymentTypeData = new PaymentTypeData(paymentTypeId, typeName, null, false, null, null, - false); - paymentDetailData = new PaymentDetailData(id, paymentTypeData, null, null, null, null, null); - } - } - - savingsAccountTransactionData = SavingsAccountTransactionData.create(transactionId, transactionType, paymentDetailData, - id, accountNo, date, currency, amount, outstandingChargeAmount, runningBalance, reversed, transSubmittedOnDate, - postInterestAsOn, cumulativeBalance, balanceEndDate); - savingsAccountTransactionData.setOverdraftAmount(overdraftAmount); - - transMap.put("id", transactionId); - if (savingsAccountData.getOfficeId() == null) { - savingsAccountData.setOfficeId(officeId); - } - - savingsAccountData.setSavingsAccountTransactionData(savingsAccountTransactionData); - } - - if (chargeId != null && !chargeDetails.containsValue(chargeId)) { - final boolean isPenalty = rs.getBoolean("isPenaltyCharge"); - final BigDecimal chargeAmount = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "chargeAmount"); - final Integer chargesTimeType = rs.getInt("chargeTimeType"); - final EnumOptionData enumOptionDataForChargesTimeType = new EnumOptionData(chargesTimeType.longValue(), null, null); - final SavingsAccountChargeData savingsAccountChargeData = new SavingsAccountChargeData(chargeId, chargeAmount, - enumOptionDataForChargesTimeType, isPenalty); - - final Long chargesPaidById = rs.getLong("chargesPaidById"); - final BigDecimal chargesPaid = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "paidByAmount"); - final SavingsAccountChargesPaidByData savingsAccountChargesPaidByData = new SavingsAccountChargesPaidByData( - chargesPaidById, chargesPaid); - savingsAccountChargesPaidByData.setSavingsAccountChargeData(savingsAccountChargeData); - if (savingsAccountChargesPaidByData != null) { - savingsAccountTransactionData.setChargesPaidByData(savingsAccountChargesPaidByData); - } - - chargeDetails.put("id", chargeId); - } - - if (taxDetailId != null && !taxDetails.containsValue(taxDetailId)) { - final BigDecimal amount = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "taxAmount"); - final BigDecimal percentage = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "taxPercentage"); - final Long debitId = rs.getLong("debitAccountId"); - final Long creditId = rs.getLong("creditAccountId"); - final GLAccountData debitAccount = GLAccountData.createFrom(debitId); - final GLAccountData creditAccount = GLAccountData.createFrom(creditId); - - if (taxComponentId != null) { - final TaxComponentData taxComponent = TaxComponentData.createTaxComponent(taxComponentId, percentage, debitAccount, - creditAccount); - savingsAccountTransactionData.setTaxDetails(new TaxDetailsData(taxComponent, amount)); - } - - taxDetails.put("id", taxDetailId); - } - - } - if (savingsAccountData != null) { - savingsAccountDataList.add(savingsAccountData); - } - return savingsAccountDataList; - - } - } - - private static final class SavingAccountMapper implements RowMapper { - - private final String schemaSql; - - SavingAccountMapper() { - final StringBuilder sqlBuilder = new StringBuilder(400); - sqlBuilder.append("sa.id as id, sa.account_no as accountNo, sa.external_id as externalId, "); - sqlBuilder.append("sa.deposit_type_enum as depositType, "); - 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 fieldOfficerId, s.display_name as fieldOfficerName, "); - sqlBuilder.append("sa.status_enum as statusEnum, "); - sqlBuilder.append("sa.sub_status_enum as subStatusEnum, "); - sqlBuilder.append("sa.reason_for_block as reasonForBlock, "); - sqlBuilder.append("sa.submittedon_date as submittedOnDate,"); - sqlBuilder.append("sbu.username as submittedByUsername,"); - sqlBuilder.append("sbu.firstname as submittedByFirstname, sbu.lastname as submittedByLastname,"); - - sqlBuilder.append("sa.rejectedon_date as rejectedOnDate,"); - sqlBuilder.append("rbu.username as rejectedByUsername,"); - sqlBuilder.append("rbu.firstname as rejectedByFirstname, rbu.lastname as rejectedByLastname,"); - - sqlBuilder.append("sa.withdrawnon_date as withdrawnOnDate,"); - sqlBuilder.append("wbu.username as withdrawnByUsername,"); - sqlBuilder.append("wbu.firstname as withdrawnByFirstname, wbu.lastname as withdrawnByLastname,"); - - sqlBuilder.append("sa.approvedon_date as approvedOnDate,"); - sqlBuilder.append("abu.username as approvedByUsername,"); - sqlBuilder.append("abu.firstname as approvedByFirstname, abu.lastname as approvedByLastname,"); - - sqlBuilder.append("sa.activatedon_date as activatedOnDate,"); - sqlBuilder.append("avbu.username as activatedByUsername,"); - sqlBuilder.append("avbu.firstname as activatedByFirstname, avbu.lastname as activatedByLastname,"); - - sqlBuilder.append("sa.closedon_date as closedOnDate,"); - sqlBuilder.append("cbu.username as closedByUsername,"); - sqlBuilder.append("cbu.firstname as closedByFirstname, cbu.lastname as closedByLastname,"); - - sqlBuilder.append( - "sa.currency_code as currencyCode, sa.currency_digits as currencyDigits, 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("sa.nominal_annual_interest_rate as nominalAnnualInterestRate, "); - sqlBuilder.append("sa.interest_compounding_period_enum as interestCompoundingPeriodType, "); - sqlBuilder.append("sa.interest_posting_period_enum as interestPostingPeriodType, "); - sqlBuilder.append("sa.interest_calculation_type_enum as interestCalculationType, "); - sqlBuilder.append("sa.interest_calculation_days_in_year_type_enum as interestCalculationDaysInYearType, "); - sqlBuilder.append("sa.min_required_opening_balance as minRequiredOpeningBalance, "); - sqlBuilder.append("sa.lockin_period_frequency as lockinPeriodFrequency,"); - sqlBuilder.append("sa.lockin_period_frequency_enum as lockinPeriodFrequencyType, "); - - sqlBuilder.append("sa.allow_overdraft as allowOverdraft, "); - sqlBuilder.append("sa.overdraft_limit as overdraftLimit, "); - sqlBuilder.append("sa.nominal_annual_interest_rate_overdraft as nominalAnnualInterestRateOverdraft, "); - sqlBuilder.append("sa.min_overdraft_for_interest_calculation as minOverdraftForInterestCalculation, "); - - sqlBuilder.append("sa.total_deposits_derived as totalDeposits, "); - sqlBuilder.append("sa.total_withdrawals_derived as totalWithdrawals, "); - sqlBuilder.append("sa.total_withdrawal_fees_derived as totalWithdrawalFees, "); - sqlBuilder.append("sa.total_annual_fees_derived as totalAnnualFees, "); - sqlBuilder.append("sa.total_interest_earned_derived as totalInterestEarned, "); - sqlBuilder.append("sa.total_interest_posted_derived as totalInterestPosted, "); - sqlBuilder.append("sa.total_overdraft_interest_derived as totalOverdraftInterestDerived, "); - sqlBuilder.append("sa.account_balance_derived as accountBalance, "); - sqlBuilder.append("sa.total_fees_charge_derived as totalFeeCharge, "); - sqlBuilder.append("sa.total_penalty_charge_derived as totalPenaltyCharge, "); - sqlBuilder.append("sa.min_balance_for_interest_calculation as minBalanceForInterestCalculation,"); - sqlBuilder.append("sa.min_required_balance as minRequiredBalance, "); - sqlBuilder.append("sa.enforce_min_required_balance as enforceMinRequiredBalance, "); - sqlBuilder.append("sa.max_allowed_lien_limit as maxAllowedLienLimit, "); - sqlBuilder.append("sa.is_lien_allowed as lienAllowed, "); - sqlBuilder.append("sa.on_hold_funds_derived as onHoldFunds, "); - sqlBuilder.append("sa.withhold_tax as withHoldTax, "); - sqlBuilder.append("sa.total_withhold_tax_derived as totalWithholdTax, "); - sqlBuilder.append("sa.last_interest_calculation_date as lastInterestCalculationDate, "); - sqlBuilder.append("sa.interest_posted_till_date as interestPostedTillDate, "); - sqlBuilder.append("sa.total_savings_amount_on_hold as onHoldAmount, "); - sqlBuilder.append("sa.withdrawal_fee_for_transfer as withdrawalFeeForTransfers, "); - sqlBuilder.append("tg.id as taxGroupId, tg.name as taxGroupName, "); - sqlBuilder.append("(select COALESCE(max(sat.transaction_date),sa.activatedon_date) "); - sqlBuilder.append("from m_savings_account_transaction as sat "); - sqlBuilder.append("where sat.is_reversed = false and sat.is_reversal = false "); - sqlBuilder.append("and sat.transaction_type_enum in (1,2) "); - sqlBuilder.append("and sat.savings_account_id = sa.id) as lastActiveTransactionDate, "); - sqlBuilder.append("sp.is_dormancy_tracking_active as isDormancyTrackingActive, "); - sqlBuilder.append("sp.days_to_inactive as daysToInactive, "); - sqlBuilder.append("sp.days_to_dormancy as daysToDormancy, "); - sqlBuilder.append("sp.days_to_escheat as daysToEscheat, "); - // CBS extended fields - sqlBuilder.append("sa.cbs_account_type as cbsAccountType, "); - sqlBuilder.append("sa.customer_segment as customerSegment, "); - sqlBuilder.append("sa.account_status as accountStatus, "); - sqlBuilder.append("sa.mode_of_operation as modeOfOperation, "); - sqlBuilder.append("sa.special_category as specialCategory, "); - sqlBuilder.append("sa.weaker_section as weakerSection, "); - sqlBuilder.append("sa.guardian_name as guardianName, "); - sqlBuilder.append("sa.guardian_cif_number as guardianCifNumber, "); - sqlBuilder.append("sa.guardian_relationship as guardianRelationship, "); - sqlBuilder.append("sa.minor_date_of_birth as minorDateOfBirth, "); - sqlBuilder.append("sa.guardian_client_id as guardianClientId, "); - sqlBuilder.append("sa.introducer_client_id as introducerClientId, "); - sqlBuilder.append("sa.introducer_name as introducerName, "); - sqlBuilder.append("sa.introducer_cif_number as introducerCifNumber, "); - sqlBuilder.append("sa.nominee_data as nomineeData, "); - sqlBuilder.append("sa.linked_account_id as linkedAccountId, "); - sqlBuilder.append("sa.linked_account_type as linkedAccountType, "); - sqlBuilder.append("sa.linked_account_no as linkedAccountNo, "); - sqlBuilder.append("sp.balance_swap_option as balanceSwapOption, "); - sqlBuilder.append("sp.msme as msme, "); - sqlBuilder.append("COALESCE(sa.nomination_facility, sp.nomination_facility) as nominationFacility, "); - sqlBuilder.append("COALESCE(sa.auto_debit_facility, sp.auto_debit_facility) as autoDebitFacility, "); - // Facility config fields - sqlBuilder.append("safc.atm_facility as atmFacility, "); - sqlBuilder.append("safc.atm_withdrawal_facility as atmWithdrawalFacility, "); - sqlBuilder.append("safc.upi_facility as upiFacility, "); - sqlBuilder.append("safc.pos_transactions_facility as posTransactionsFacility, "); - sqlBuilder.append("safc.cheque_book as chequeBook, "); - sqlBuilder.append("safc.imps_facility as impsFacility, "); - sqlBuilder.append("safc.sms_alerts as smsAlerts, "); - sqlBuilder.append("safc.bbps_facility as bbpsFacility, "); - sqlBuilder.append("safc.insurance_cover as insuranceCover, "); - sqlBuilder.append("safc.bepg_facility as bepgFacility, "); - sqlBuilder.append("safc.neft_facility as neftFacility, "); - sqlBuilder.append("safc.rtgs_facility as rtgsFacility, "); - sqlBuilder.append("safc.inter_bank_total_cap_facility as interBankTotalCapFacility, "); - sqlBuilder.append("safc.withdrawal_facility as withdrawalFacility, "); - sqlBuilder.append("safc.deposit_facility_kyc as depositFacilityKyc, "); - sqlBuilder.append("safc.virtual_debit_card as virtualDebitCard "); - 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 "); - sqlBuilder.append("left join m_appuser sbu on sbu.id = sa.submittedon_userid "); - sqlBuilder.append("left join m_appuser rbu on rbu.id = sa.rejectedon_userid "); - sqlBuilder.append("left join m_appuser wbu on wbu.id = sa.withdrawnon_userid "); - sqlBuilder.append("left join m_appuser abu on abu.id = sa.approvedon_userid "); - sqlBuilder.append("left join m_appuser avbu on avbu.id = sa.activatedon_userid "); - sqlBuilder.append("left join m_appuser cbu on cbu.id = sa.closedon_userid "); - sqlBuilder.append("left join m_tax_group tg on tg.id = sa.tax_group_id "); - sqlBuilder.append("left join m_savings_account_facility_config safc on safc.savings_account_id = sa.id "); - - this.schemaSql = sqlBuilder.toString(); - } - - public String schema() { - return this.schemaSql; - } - - @Override - public SavingsAccountData 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 Integer depositTypeId = rs.getInt("depositType"); - final EnumOptionData depositType = SavingsEnumerations.depositType(depositTypeId); - - 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 Integer statusEnum = JdbcSupport.getInteger(rs, "statusEnum"); - final SavingsAccountStatusEnumData status = SavingsEnumerations.status(statusEnum); - - final Integer subStatusEnum = JdbcSupport.getInteger(rs, "subStatusEnum"); - final SavingsAccountSubStatusEnumData subStatus = SavingsEnumerations.subStatus(subStatusEnum); - final String reasonForBlock = rs.getString("reasonForBlock"); - - final LocalDate lastActiveTransactionDate = JdbcSupport.getLocalDate(rs, "lastActiveTransactionDate"); - final boolean isDormancyTrackingActive = rs.getBoolean("isDormancyTrackingActive"); - final Integer numDaysToInactive = JdbcSupport.getInteger(rs, "daysToInactive"); - final Integer numDaysToDormancy = JdbcSupport.getInteger(rs, "daysToDormancy"); - final Integer numDaysToEscheat = JdbcSupport.getInteger(rs, "daysToEscheat"); - Integer daysToInactive = null; - Integer daysToDormancy = null; - Integer daysToEscheat = null; - - LocalDate currentDate = DateUtils.getBusinessLocalDate(); - if (isDormancyTrackingActive && statusEnum.equals(SavingsAccountStatusType.ACTIVE.getValue())) { - if (subStatusEnum < SavingsAccountSubStatusEnum.ESCHEAT.getValue()) { - daysToEscheat = DateUtils.getExactDifferenceInDays(currentDate, lastActiveTransactionDate.plusDays(numDaysToEscheat)); - } - if (subStatusEnum < SavingsAccountSubStatusEnum.DORMANT.getValue()) { - daysToDormancy = DateUtils.getExactDifferenceInDays(currentDate, lastActiveTransactionDate.plusDays(numDaysToDormancy)); - } - if (subStatusEnum < SavingsAccountSubStatusEnum.INACTIVE.getValue()) { - daysToInactive = DateUtils.getExactDifferenceInDays(currentDate, lastActiveTransactionDate.plusDays(numDaysToInactive)); - } - } - - final LocalDate submittedOnDate = JdbcSupport.getLocalDate(rs, "submittedOnDate"); - final String submittedByUsername = rs.getString("submittedByUsername"); - final String submittedByFirstname = rs.getString("submittedByFirstname"); - final String submittedByLastname = rs.getString("submittedByLastname"); - - final LocalDate rejectedOnDate = JdbcSupport.getLocalDate(rs, "rejectedOnDate"); - final String rejectedByUsername = rs.getString("rejectedByUsername"); - final String rejectedByFirstname = rs.getString("rejectedByFirstname"); - final String rejectedByLastname = rs.getString("rejectedByLastname"); - - final LocalDate withdrawnOnDate = JdbcSupport.getLocalDate(rs, "withdrawnOnDate"); - final String withdrawnByUsername = rs.getString("withdrawnByUsername"); - final String withdrawnByFirstname = rs.getString("withdrawnByFirstname"); - final String withdrawnByLastname = rs.getString("withdrawnByLastname"); - - final LocalDate approvedOnDate = JdbcSupport.getLocalDate(rs, "approvedOnDate"); - final String approvedByUsername = rs.getString("approvedByUsername"); - final String approvedByFirstname = rs.getString("approvedByFirstname"); - final String approvedByLastname = rs.getString("approvedByLastname"); - - final LocalDate activatedOnDate = JdbcSupport.getLocalDate(rs, "activatedOnDate"); - final String activatedByUsername = rs.getString("activatedByUsername"); - final String activatedByFirstname = rs.getString("activatedByFirstname"); - final String activatedByLastname = rs.getString("activatedByLastname"); - - final LocalDate closedOnDate = JdbcSupport.getLocalDate(rs, "closedOnDate"); - final String closedByUsername = rs.getString("closedByUsername"); - final String closedByFirstname = rs.getString("closedByFirstname"); - final String closedByLastname = rs.getString("closedByLastname"); - - final SavingsAccountApplicationTimelineData timeline = new SavingsAccountApplicationTimelineData(submittedOnDate, - submittedByUsername, submittedByFirstname, submittedByLastname, rejectedOnDate, rejectedByUsername, rejectedByFirstname, - rejectedByLastname, withdrawnOnDate, withdrawnByUsername, withdrawnByFirstname, withdrawnByLastname, approvedOnDate, - approvedByUsername, approvedByFirstname, approvedByLastname, activatedOnDate, activatedByUsername, activatedByFirstname, - activatedByLastname, closedOnDate, closedByUsername, closedByFirstname, closedByLastname); - - final String currencyCode = rs.getString("currencyCode"); - final String currencyName = rs.getString("currencyName"); - final String currencyNameCode = rs.getString("currencyNameCode"); - final String currencyDisplaySymbol = rs.getString("currencyDisplaySymbol"); - final Integer currencyDigits = JdbcSupport.getInteger(rs, "currencyDigits"); - final Integer inMultiplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf"); - final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMultiplesOf, currencyDisplaySymbol, - currencyNameCode); - - final BigDecimal nominalAnnualInterestRate = rs.getBigDecimal("nominalAnnualInterestRate"); - - final EnumOptionData interestCompoundingPeriodType = SavingsEnumerations.compoundingInterestPeriodType( - SavingsCompoundingInterestPeriodType.fromInt(JdbcSupport.getInteger(rs, "interestCompoundingPeriodType"))); - - final EnumOptionData interestPostingPeriodType = SavingsEnumerations.interestPostingPeriodType( - SavingsPostingInterestPeriodType.fromInt(JdbcSupport.getInteger(rs, "interestPostingPeriodType"))); - - final EnumOptionData interestCalculationType = SavingsEnumerations - .interestCalculationType(SavingsInterestCalculationType.fromInt(JdbcSupport.getInteger(rs, "interestCalculationType"))); - - final EnumOptionData interestCalculationDaysInYearType = SavingsEnumerations.interestCalculationDaysInYearType( - SavingsInterestCalculationDaysInYearType.fromInt(JdbcSupport.getInteger(rs, "interestCalculationDaysInYearType"))); - - final BigDecimal minRequiredOpeningBalance = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "minRequiredOpeningBalance"); - - final Integer lockinPeriodFrequency = JdbcSupport.getInteger(rs, "lockinPeriodFrequency"); - EnumOptionData lockinPeriodFrequencyType = null; - final Integer lockinPeriodFrequencyTypeValue = JdbcSupport.getInteger(rs, "lockinPeriodFrequencyType"); - if (lockinPeriodFrequencyTypeValue != null) { - final SavingsPeriodFrequencyType lockinPeriodType = SavingsPeriodFrequencyType.fromInt(lockinPeriodFrequencyTypeValue); - lockinPeriodFrequencyType = SavingsEnumerations.lockinPeriodFrequencyType(lockinPeriodType); - } - - final boolean withdrawalFeeForTransfers = rs.getBoolean("withdrawalFeeForTransfers"); - - final boolean allowOverdraft = rs.getBoolean("allowOverdraft"); - final BigDecimal overdraftLimit = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "overdraftLimit"); - final BigDecimal nominalAnnualInterestRateOverdraft = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, - "nominalAnnualInterestRateOverdraft"); - final BigDecimal minOverdraftForInterestCalculation = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, - "minOverdraftForInterestCalculation"); - - final BigDecimal minRequiredBalance = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "minRequiredBalance"); - final boolean enforceMinRequiredBalance = rs.getBoolean("enforceMinRequiredBalance"); - - final BigDecimal maxAllowedLienLimit = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "maxAllowedLienLimit"); - final boolean lienAllowed = rs.getBoolean("lienAllowed"); - - final BigDecimal totalDeposits = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalDeposits"); - final BigDecimal totalWithdrawals = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalWithdrawals"); - final BigDecimal totalWithdrawalFees = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalWithdrawalFees"); - final BigDecimal totalAnnualFees = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalAnnualFees"); - - final BigDecimal totalInterestEarned = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalInterestEarned"); - final BigDecimal totalInterestPosted = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "totalInterestPosted"); - final BigDecimal accountBalance = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "accountBalance"); - final BigDecimal totalFeeCharge = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalFeeCharge"); - final BigDecimal totalPenaltyCharge = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalPenaltyCharge"); - final BigDecimal totalOverdraftInterestDerived = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, - "totalOverdraftInterestDerived"); - final BigDecimal totalWithholdTax = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalWithholdTax"); - final LocalDate interestPostedTillDate = JdbcSupport.getLocalDate(rs, "interestPostedTillDate"); - - final BigDecimal minBalanceForInterestCalculation = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, - "minBalanceForInterestCalculation"); - final BigDecimal onHoldFunds = rs.getBigDecimal("onHoldFunds"); - - final BigDecimal onHoldAmount = rs.getBigDecimal("onHoldAmount"); - - BigDecimal availableBalance = accountBalance; - if (availableBalance != null && onHoldFunds != null) { - availableBalance = availableBalance.subtract(onHoldFunds); - } - - if (availableBalance != null && onHoldAmount != null) { - - availableBalance = availableBalance.subtract(onHoldAmount); - } - - BigDecimal interestNotPosted = BigDecimal.ZERO; - LocalDate lastInterestCalculationDate = null; - if (totalInterestEarned != null) { - interestNotPosted = totalInterestEarned.subtract(totalInterestPosted).add(totalOverdraftInterestDerived); - lastInterestCalculationDate = JdbcSupport.getLocalDate(rs, "lastInterestCalculationDate"); - } - - final SavingsAccountSummaryData summary = new SavingsAccountSummaryData(currency, totalDeposits, totalWithdrawals, - totalWithdrawalFees, totalAnnualFees, totalInterestEarned, totalInterestPosted, accountBalance, totalFeeCharge, - totalPenaltyCharge, totalOverdraftInterestDerived, totalWithholdTax, interestNotPosted, lastInterestCalculationDate, - availableBalance, interestPostedTillDate); - - final boolean withHoldTax = rs.getBoolean("withHoldTax"); - final Long taxGroupId = JdbcSupport.getLong(rs, "taxGroupId"); - final String taxGroupName = rs.getString("taxGroupName"); - TaxGroupData taxGroupData = null; - if (taxGroupId != null) { - taxGroupData = TaxGroupData.lookup(taxGroupId, taxGroupName); - } - - final SavingsAccountData accountData = SavingsAccountData.instance(id, accountNo, depositType, externalId, groupId, groupName, - clientId, clientName, productId, productName, fieldOfficerId, fieldOfficerName, status, subStatus, reasonForBlock, - timeline, currency, nominalAnnualInterestRate, interestCompoundingPeriodType, interestPostingPeriodType, - interestCalculationType, interestCalculationDaysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency, - lockinPeriodFrequencyType, withdrawalFeeForTransfers, summary, allowOverdraft, overdraftLimit, minRequiredBalance, - enforceMinRequiredBalance, maxAllowedLienLimit, lienAllowed, minBalanceForInterestCalculation, onHoldFunds, - nominalAnnualInterestRateOverdraft, minOverdraftForInterestCalculation, withHoldTax, taxGroupData, - lastActiveTransactionDate, isDormancyTrackingActive, daysToInactive, daysToDormancy, daysToEscheat, onHoldAmount); - - // CBS extended fields - accountData.setCbsAccountType(rs.getString("cbsAccountType")); - accountData.setCustomerSegment(rs.getString("customerSegment")); - accountData.setAccountStatus(rs.getString("accountStatus")); - accountData.setModeOfOperation(rs.getString("modeOfOperation")); - accountData.setSpecialCategory(rs.getString("specialCategory")); - accountData.setWeakerSection(rs.getString("weakerSection")); - accountData.setGuardianName(rs.getString("guardianName")); - accountData.setGuardianCifNumber(rs.getString("guardianCifNumber")); - accountData.setGuardianRelationship(rs.getString("guardianRelationship")); - accountData.setMinorDateOfBirth(JdbcSupport.getLocalDate(rs, "minorDateOfBirth")); - accountData.setGuardianClientId(JdbcSupport.getLong(rs, "guardianClientId")); - accountData.setIntroducerClientId(JdbcSupport.getLong(rs, "introducerClientId")); - accountData.setIntroducerName(rs.getString("introducerName")); - accountData.setIntroducerCifNumber(rs.getString("introducerCifNumber")); - accountData.setLinkedAccountId(JdbcSupport.getLong(rs, "linkedAccountId")); - accountData.setLinkedAccountType(rs.getString("linkedAccountType")); - accountData.setLinkedAccountNo(rs.getString("linkedAccountNo")); - accountData.setBalanceSwapOption(rs.getObject("balanceSwapOption") != null ? rs.getBoolean("balanceSwapOption") : null); - accountData.setMsme(rs.getObject("msme") != null ? rs.getBoolean("msme") : null); - accountData.setNominationFacility(rs.getObject("nominationFacility") != null ? rs.getBoolean("nominationFacility") : null); - accountData.setAutoDebitFacility(rs.getObject("autoDebitFacility") != null ? rs.getBoolean("autoDebitFacility") : null); - - // Nominee as parsed map - final String nomineeDataStr = rs.getString("nomineeData"); - if (nomineeDataStr != null && !nomineeDataStr.isEmpty()) { - try { - com.google.gson.Gson gson = new com.google.gson.Gson(); - @SuppressWarnings("unchecked") - java.util.Map nomineeMap = gson.fromJson(nomineeDataStr, - new com.google.gson.reflect.TypeToken>() {}.getType()); - accountData.setNomineeData(nomineeMap); - } catch (Exception ignored) {} - } - - // Facility config - final boolean hasFacilityConfig = rs.getObject("atmFacility") != null; - if (hasFacilityConfig) { - final java.util.Map facilityConfigMap = new java.util.LinkedHashMap<>(); - facilityConfigMap.put("atmFacility", rs.getBoolean("atmFacility")); - facilityConfigMap.put("atmWithdrawalFacility", rs.getBoolean("atmWithdrawalFacility")); - facilityConfigMap.put("upiFacility", rs.getBoolean("upiFacility")); - facilityConfigMap.put("posTransactionsFacility", rs.getBoolean("posTransactionsFacility")); - facilityConfigMap.put("chequeBook", rs.getBoolean("chequeBook")); - facilityConfigMap.put("impsFacility", rs.getBoolean("impsFacility")); - facilityConfigMap.put("smsAlerts", rs.getBoolean("smsAlerts")); - facilityConfigMap.put("bbpsFacility", rs.getBoolean("bbpsFacility")); - facilityConfigMap.put("insuranceCover", rs.getBoolean("insuranceCover")); - facilityConfigMap.put("bepgFacility", rs.getBoolean("bepgFacility")); - facilityConfigMap.put("neftFacility", rs.getBoolean("neftFacility")); - facilityConfigMap.put("rtgsFacility", rs.getBoolean("rtgsFacility")); - facilityConfigMap.put("interBankTotalCapFacility", rs.getBoolean("interBankTotalCapFacility")); - facilityConfigMap.put("withdrawalFacility", rs.getBoolean("withdrawalFacility")); - facilityConfigMap.put("depositFacilityKyc", rs.getBoolean("depositFacilityKyc")); - facilityConfigMap.put("virtualDebitCard", rs.getBoolean("virtualDebitCard")); - accountData.setAccountFacilityConfig(facilityConfigMap); - } - - return accountData; - } - } - - private static final class SavingAccountMapperForLookup implements RowMapper { - - private final String schemaSql; - - SavingAccountMapperForLookup() { - final StringBuilder sqlBuilder = new StringBuilder(400); - sqlBuilder.append("sa.id as id, sa.account_no as accountNo, "); - sqlBuilder.append("sa.deposit_type_enum as depositType, "); - sqlBuilder.append("sp.id as productId, sp.name as productName, "); - sqlBuilder.append("sa.status_enum as statusEnum "); - - sqlBuilder.append("from m_savings_account sa "); - sqlBuilder.append("join m_savings_product sp ON sa.product_id = sp.id "); - - this.schemaSql = sqlBuilder.toString(); - } - - public String schema() { - return this.schemaSql; - } - - @Override - public SavingsAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { - - final Long id = rs.getLong("id"); - final String accountNo = rs.getString("accountNo"); - final Integer depositTypeId = rs.getInt("depositType"); - final EnumOptionData depositType = SavingsEnumerations.depositType(depositTypeId); - - final Long productId = rs.getLong("productId"); - final String productName = rs.getString("productName"); - - final Integer statusEnum = JdbcSupport.getInteger(rs, "statusEnum"); - final SavingsAccountStatusEnumData status = SavingsEnumerations.status(statusEnum); - - return SavingsAccountData.lookupWithProductDetails(id, accountNo, depositType, productId, productName, status); - } - } - - @Override - public SavingsAccountTransactionData retrieveDepositTransactionTemplate(final Long savingsId, - final DepositAccountType depositAccountType) { - - try { - final String sql = "select " + this.transactionTemplateMapper.schema() + " where sa.id = ? and sa.deposit_type_enum = ?"; - - return this.jdbcTemplate.queryForObject(sql, this.transactionTemplateMapper, // NOSONAR - new Object[] { savingsId, depositAccountType.getValue() }); - } catch (final EmptyResultDataAccessException e) { - throw new SavingsAccountNotFoundException(savingsId, e); - } - } - - @Override - public Collection retrieveAllTransactions(final Long savingsId, DepositAccountType depositAccountType) { - // #audit backward compatibility - final String sql = "select " + this.transactionsMapper.schema() - + " where sa.id = ? and sa.deposit_type_enum = ? order by tr.transaction_date DESC," + " tr." + CREATED_DATE_DB_FIELD - + " DESC, tr.created_date DESC, tr.id DESC"; - return this.jdbcTemplate.query(sql, this.transactionsMapper, new Object[] { savingsId, depositAccountType.getValue() }); // NOSONAR - } - - @Override - public SavingsAccountTransactionData retrieveSavingsTransaction(final Long savingsId, final Long transactionId, - DepositAccountType depositAccountType) { - - final String sql = "select " + this.transactionsMapper.schema() + " where sa.id = ? and sa.deposit_type_enum = ? and tr.id= ?"; - - return this.jdbcTemplate.queryForObject(sql, this.transactionsMapper, // NOSONAR - new Object[] { savingsId, depositAccountType.getValue(), transactionId }); - } - - private static final class SavingsAccountTransactionsForBatchMapper implements RowMapper { - - private final String schemaSql; - - SavingsAccountTransactionsForBatchMapper() { - - final StringBuilder sqlBuilder = new StringBuilder(400); - sqlBuilder.append("tr.id as transactionId, tr.ref_no as refNo "); - sqlBuilder.append("from m_savings_account_transaction tr"); - - this.schemaSql = sqlBuilder.toString(); - } - - public String schema() { - return this.schemaSql; - } - - @Override - public SavingsAccountTransactionData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { - final Long id = rs.getLong("transactionId"); - SavingsAccountTransactionData savingsAccountTransactionData = SavingsAccountTransactionData.create(id); - final String refNo = rs.getString("refNo"); - savingsAccountTransactionData.setRefNo(refNo); - return savingsAccountTransactionData; - } - } - - /* - * @Override public Collection retrieveAccountsWithAnnualFeeDue() { final String sql = - * "select " + this.annualFeeMapper.schema() + - * " where sa.annual_fee_next_due_date is not null and sa.annual_fee_next_due_date <= NOW()" ; - * - * return this.jdbcTemplate.query(sql, this.annualFeeMapper, new Object[] {}); } - */ - - public static class SavingsAccountTransactionsMapper implements RowMapper { - - private static final String SELECT = buildSelect(); - private static final String FROM = buildFrom(); - private static final String SCHEMA = SELECT + FROM; - - public SavingsAccountTransactionsMapper() {} - - protected static String buildSelect() { - return "tr.id as transactionId, tr.transaction_type_enum as transactionType, " - + "tr.transaction_date as transactionDate, tr.amount as transactionAmount, " - + "tr.release_id_of_hold_amount as releaseTransactionId, tr.reason_for_block as reasonForBlock, " - + "tr.submitted_on_date as submittedOnDate, au.username as submittedByUsername, nt.note as transactionNote, " - + "tr.running_balance_derived as runningBalance, tr.is_reversed as reversed, " - + "tr.is_reversal as isReversal, tr.original_transaction_id as originalTransactionId, tr.is_lien_transaction as lienTransaction, " - + "fromtran.id as fromTransferId, fromtran.is_reversed as fromTransferReversed, " - + "fromtran.transaction_date as fromTransferDate, fromtran.amount as fromTransferAmount, " - + "fromtran.description as fromTransferDescription, " - + "totran.id as toTransferId, totran.is_reversed as toTransferReversed, " - + "totran.transaction_date as toTransferDate, totran.amount as toTransferAmount, " - + "totran.description as toTransferDescription, sa.id as savingsId, sa.account_no as accountNo, " - + "pd.payment_type_id as paymentType,pd.account_number as accountNumber,pd.check_number as checkNumber, " - + "pd.receipt_number as receiptNumber, pd.bank_number as bankNumber,pd.routing_code as routingCode, " - + "sa.currency_code as currencyCode, sa.currency_digits as currencyDigits, sa.currency_multiplesof as inMultiplesOf, " - + "curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, " - + "curr.display_symbol as currencyDisplaySymbol, pt.value as paymentTypeName, " + "tr.is_manual as postInterestAsOn "; - } - - protected static String buildFrom() { - return " FROM m_savings_account_transaction tr join m_savings_account sa on tr.savings_account_id = sa.id " - + "join m_currency curr on curr.code = sa.currency_code " - + "left join m_account_transfer_transaction fromtran on fromtran.from_savings_transaction_id = tr.id " - + "left join m_account_transfer_transaction totran on totran.to_savings_transaction_id = tr.id " - + "left join m_payment_detail pd on tr.payment_detail_id = pd.id " - + "left join m_payment_type pt on pd.payment_type_id = pt.id left join m_appuser au on au.id= tr." + CREATED_BY_DB_FIELD - + " left join m_note nt ON nt.savings_account_transaction_id=tr.id "; - } - - public String schema() { - return SCHEMA; - } - - public String select() { - return SELECT; - } - - public String from() { - return FROM; - } - - @Override - public SavingsAccountTransactionData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { - final Long id = rs.getLong("transactionId"); - final int transactionTypeInt = JdbcSupport.getInteger(rs, "transactionType"); - final SavingsAccountTransactionEnumData transactionType = SavingsEnumerations.transactionType(transactionTypeInt); - - final LocalDate date = JdbcSupport.getLocalDate(rs, "transactionDate"); - final LocalDate submittedOnDate = JdbcSupport.getLocalDate(rs, "submittedOnDate"); - final BigDecimal amount = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "transactionAmount"); - final Long releaseTransactionId = rs.getLong("releaseTransactionId"); - final String reasonForBlock = rs.getString("reasonForBlock"); - final BigDecimal outstandingChargeAmount = null; - final BigDecimal runningBalance = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "runningBalance"); - final boolean reversed = rs.getBoolean("reversed"); - final boolean isReversal = rs.getBoolean("isReversal"); - final Long originalTransactionId = rs.getLong("originalTransactionId"); - final Boolean lienTransaction = rs.getBoolean("lienTransaction"); - - final Long savingsId = rs.getLong("savingsId"); - final String accountNo = rs.getString("accountNo"); - final boolean postInterestAsOn = rs.getBoolean("postInterestAsOn"); - - PaymentDetailData paymentDetailData = null; - if (transactionType.isDepositOrWithdrawal()) { - final Long paymentTypeId = JdbcSupport.getLong(rs, "paymentType"); - if (paymentTypeId != null) { - final String typeName = rs.getString("paymentTypeName"); - final PaymentTypeData paymentType = PaymentTypeData.builder().id(paymentTypeId).name(typeName).build(); - final String accountNumber = rs.getString("accountNumber"); - final String checkNumber = rs.getString("checkNumber"); - final String routingCode = rs.getString("routingCode"); - final String receiptNumber = rs.getString("receiptNumber"); - final String bankNumber = rs.getString("bankNumber"); - paymentDetailData = new PaymentDetailData(id, paymentType, accountNumber, checkNumber, routingCode, receiptNumber, - bankNumber); - } - } - - 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); - - AccountTransferData transfer = null; - final Long fromTransferId = JdbcSupport.getLong(rs, "fromTransferId"); - final Long toTransferId = JdbcSupport.getLong(rs, "toTransferId"); - if (fromTransferId != null) { - final LocalDate fromTransferDate = JdbcSupport.getLocalDate(rs, "fromTransferDate"); - final BigDecimal fromTransferAmount = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "fromTransferAmount"); - final boolean fromTransferReversed = rs.getBoolean("fromTransferReversed"); - final String fromTransferDescription = rs.getString("fromTransferDescription"); - - transfer = AccountTransferData.transferBasicDetails(fromTransferId, currency, fromTransferAmount, fromTransferDate, - fromTransferDescription, fromTransferReversed); - } else if (toTransferId != null) { - final LocalDate toTransferDate = JdbcSupport.getLocalDate(rs, "toTransferDate"); - final BigDecimal toTransferAmount = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "toTransferAmount"); - final boolean toTransferReversed = rs.getBoolean("toTransferReversed"); - final String toTransferDescription = rs.getString("toTransferDescription"); - - transfer = AccountTransferData.transferBasicDetails(toTransferId, currency, toTransferAmount, toTransferDate, - toTransferDescription, toTransferReversed); - } - final String submittedByUsername = rs.getString("submittedByUsername"); - final String note = rs.getString("transactionNote"); - return SavingsAccountTransactionData.create(id, transactionType, paymentDetailData, savingsId, accountNo, date, currency, - amount, outstandingChargeAmount, runningBalance, reversed, transfer, submittedOnDate, postInterestAsOn, - submittedByUsername, note, isReversal, originalTransactionId, lienTransaction, releaseTransactionId, reasonForBlock); - } - } - - private static final class SavingsAccountTransactionTemplateMapper implements RowMapper { - - private final String schemaSql; - - SavingsAccountTransactionTemplateMapper() { - final StringBuilder sqlBuilder = new StringBuilder(400); - sqlBuilder.append("sa.id as id, sa.account_no as accountNo, "); - sqlBuilder.append( - "sa.currency_code as currencyCode, sa.currency_digits as currencyDigits, 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("sa.min_required_opening_balance as minRequiredOpeningBalance "); - sqlBuilder.append("from m_savings_account sa "); - sqlBuilder.append("join m_currency curr on curr.code = sa.currency_code "); - - this.schemaSql = sqlBuilder.toString(); - } - - public String schema() { - return this.schemaSql; - } - - @Override - public SavingsAccountTransactionData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { - - final Long savingsId = rs.getLong("id"); - final String accountNo = rs.getString("accountNo"); - - 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 SavingsAccountTransactionData.template(savingsId, accountNo, DateUtils.getBusinessLocalDate(), currency); - } - } - - @Override - public Collection retrieveForLookup(Long clientId, Boolean overdraft) { - - SavingAccountMapperForLookup accountMapperForLookup = new SavingAccountMapperForLookup(); - final StringBuilder sqlBuilder = new StringBuilder("select " + accountMapperForLookup.schema()); - sqlBuilder.append(" where sa.client_id = ? and sa.status_enum = 300"); - Object[] queryParameters = null; - if (overdraft == null) { - queryParameters = new Object[] { clientId }; - } else { - sqlBuilder.append(" and sa.allow_overdraft = ?"); - queryParameters = new Object[] { clientId, overdraft }; - } - return this.jdbcTemplate.query(sqlBuilder.toString(), accountMapperForLookup, queryParameters); - - } - - @Override - public List retrieveSavingsIdsPendingInactive(LocalDate tenantLocalDate) { - List ret = null; - StringBuilder sql = new StringBuilder("select sa.id "); - sql.append(" from m_savings_account as sa "); - sql.append(" inner join m_savings_product as sp on (sa.product_id = sp.id and sp.is_dormancy_tracking_active = true) "); - sql.append(" where sa.status_enum = 300 "); - sql.append(" and sa.sub_status_enum = 0 "); - String compareDate = "(select COALESCE(max(sat.transaction_date), sa.activatedon_date) " - + "from m_savings_account_transaction as sat where sat.is_reversed = false and sat.is_reversal = false" - + " and sat.transaction_type_enum in (1,2) and sat.savings_account_id = sa.id)"; - sql.append(" and ").append(sqlGenerator.dateDiff("?", compareDate)).append(" >= sp.days_to_inactive "); - - try { - ret = this.jdbcTemplate.queryForList(sql.toString(), new Object[] { tenantLocalDate }, Long.class); - } catch (EmptyResultDataAccessException e) { - // ignore empty result scenario - } catch (DataAccessException e) { - throw e; - } - - return ret; - } - - @Override - public List retrieveSavingsIdsPendingDormant(LocalDate tenantLocalDate) { - List ret = null; - StringBuilder sql = new StringBuilder("select sa.id "); - sql.append(" from m_savings_account as sa "); - sql.append(" inner join m_savings_product as sp on (sa.product_id = sp.id and sp.is_dormancy_tracking_active = true) "); - sql.append(" where sa.status_enum = 300 "); - sql.append(" and sa.sub_status_enum = 100 "); - sql.append(" and " + sqlGenerator.dateDiff("?", - "(select COALESCE(max(sat.transaction_date),sa.activatedon_date) from m_savings_account_transaction as sat where sat.is_reversed = false and sat.is_reversal = false and sat.transaction_type_enum in (1,2) and sat.savings_account_id = sa.id)") - + " "); - sql.append(" >= sp.days_to_dormancy "); - - try { - ret = this.jdbcTemplate.queryForList(sql.toString(), new Object[] { tenantLocalDate }, Long.class); - } catch (EmptyResultDataAccessException e) { - // ignore empty result scenario - } catch (DataAccessException e) { - throw e; - } - - return ret; - } - - @Override - public List retrieveSavingsIdsPendingEscheat(LocalDate tenantLocalDate) { - List ret = null; - StringBuilder sql = new StringBuilder("select sa.id "); - sql.append(" from m_savings_account as sa "); - sql.append(" inner join m_savings_product as sp on (sa.product_id = sp.id and sp.is_dormancy_tracking_active = true) "); - sql.append(" where sa.status_enum = 300 "); - sql.append(" and sa.sub_status_enum = 200 "); - sql.append(" and " + sqlGenerator.dateDiff("?", - "(select COALESCE(max(sat.transaction_date),sa.activatedon_date) from m_savings_account_transaction as sat where sat.is_reversed = false and sat.is_reversal = false and sat.transaction_type_enum in (1,2) and sat.savings_account_id = sa.id)") - + " "); - sql.append(" >= sp.days_to_escheat "); - - try { - ret = this.jdbcTemplate.queryForList(sql.toString(), Long.class, new Object[] { tenantLocalDate }); - } catch (EmptyResultDataAccessException e) { - // ignore empty result scenario - } catch (DataAccessException e) { - throw e; - } - - return ret; - } - - @Override - public boolean isAccountBelongsToClient(final Long clientId, final Long accountId, final DepositAccountType depositAccountType, - final String currencyCode) { - try { - final StringBuilder buff = new StringBuilder("select count(*) from m_savings_account sa "); - buff.append( - " where sa.id = ? and sa.client_id = ? and sa.deposit_type_enum = ? and sa.currency_code = ? and sa.status_enum = 300"); - return this.jdbcTemplate.queryForObject(buff.toString(), Integer.class, accountId, clientId, depositAccountType.getValue(), - currencyCode) > 0; - } catch (final EmptyResultDataAccessException e) { - throw new SavingsAccountNotFoundException(accountId, e); - } - } - /* - * private static final class SavingsAccountAnnualFeeMapper implements RowMapper { - * - * private final String schemaSql; - * - * public SavingsAccountAnnualFeeMapper() { final StringBuilder sqlBuilder = new StringBuilder(200); - * sqlBuilder.append("sa.id as id, sa.account_no as accountNo, "); sqlBuilder - * .append("sa.annual_fee_next_due_date as annualFeeNextDueDate "); sqlBuilder.append("from m_savings_account sa "); - * - * this.schemaSql = sqlBuilder.toString(); } - * - * public String schema() { return this.schemaSql; } - * - * @Override public SavingsAccountAnnualFeeData mapRow(final ResultSet rs, - * - * @SuppressWarnings("unused") final int rowNum) throws SQLException { - * - * final Long id = rs.getLong("id"); final String accountNo = rs.getString("accountNo"); final LocalDate - * annualFeeNextDueDate = JdbcSupport.getLocalDate(rs, "annualFeeNextDueDate"); - * - * return SavingsAccountAnnualFeeData.instance(id, accountNo, annualFeeNextDueDate); } } - */ - - @Override - public String retrieveAccountNumberByAccountId(Long accountId) { - try { - final String sql = "select s.account_no from m_savings_account s where s.id = ?"; - return this.jdbcTemplate.queryForObject(sql, String.class, accountId); - } catch (final EmptyResultDataAccessException e) { - throw new SavingsAccountNotFoundException(accountId, e); - } - } - - @Override - public List getAccountsIdsByStatusPaged(Integer status, int pageSize, Long maxSavingsIdInList) { - String sql = new StringBuilder().append(" SELECT sa.id FROM m_savings_account sa ") - .append(" where sa.id > ? and sa.status_enum = ? ").append(" order by sa.id limit ?").toString(); - - try { - return this.jdbcTemplate.queryForList(sql, Long.class, new Object[] { maxSavingsIdInList, status, pageSize }); - } catch (EmptyResultDataAccessException e) { - return new ArrayList<>(); - } - } - - @Override - public Long retrieveAccountIdByExternalId(final ExternalId externalId) { - return savingsAccountRepositoryWrapper.findIdByExternalId(externalId); - } - - @Override - public List retrievePeriodicAccrualData(LocalDate tillDate, SavingsAccount savings) { - Long savingsId = (savings != null) ? savings.getId() : null; - Integer status = SavingsAccountStatusType.ACTIVE.getValue(); - Integer accountingRule = AccountingRuleType.ACCRUAL_PERIODIC.getValue(); - - return this.savingsAccountRepositoryWrapper.findAccrualData(tillDate, savingsId, status, accountingRule); - } -} +/** + * 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.infrastructure.core.domain.AuditableFieldsConstants.CREATED_BY_DB_FIELD; +import static org.apache.fineract.infrastructure.core.domain.AuditableFieldsConstants.CREATED_DATE_DB_FIELD; + +import java.math.BigDecimal; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.accounting.common.AccountingRuleType; +import org.apache.fineract.accounting.glaccount.data.GLAccountData; +import org.apache.fineract.infrastructure.core.data.EnumOptionData; +import org.apache.fineract.infrastructure.core.domain.ExternalId; +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.service.PlatformSecurityContext; +import org.apache.fineract.infrastructure.security.utils.ColumnValidator; +import org.apache.fineract.organisation.monetary.data.CurrencyData; +import org.apache.fineract.portfolio.account.data.AccountTransferData; +import org.apache.fineract.portfolio.client.data.ClientData; +import org.apache.fineract.portfolio.group.data.GroupGeneralData; +import org.apache.fineract.portfolio.paymentdetail.data.PaymentDetailData; +import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData; +import org.apache.fineract.portfolio.savings.DepositAccountType; +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.SavingsAccountApplicationTimelineData; +import org.apache.fineract.portfolio.savings.data.SavingsAccountChargeData; +import org.apache.fineract.portfolio.savings.data.SavingsAccountData; +import org.apache.fineract.portfolio.savings.data.SavingsAccountStatusEnumData; +import org.apache.fineract.portfolio.savings.data.SavingsAccountSubStatusEnumData; +import org.apache.fineract.portfolio.savings.data.SavingsAccountSummaryData; +import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData; +import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionEnumData; +import org.apache.fineract.portfolio.savings.data.SavingsAccrualData; +import org.apache.fineract.portfolio.savings.data.SavingsProductData; +import org.apache.fineract.portfolio.savings.domain.SavingsAccount; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountChargesPaidByData; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountStatusType; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountSubStatusEnum; +import org.apache.fineract.portfolio.savings.exception.SavingsAccountNotFoundException; +import org.apache.fineract.portfolio.tax.data.TaxComponentData; +import org.apache.fineract.portfolio.tax.data.TaxDetailsData; +import org.apache.fineract.portfolio.tax.data.TaxGroupData; +import org.apache.fineract.useradministration.domain.AppUser; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.ResultSetExtractor; +import org.springframework.jdbc.core.RowMapper; + +public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountReadPlatformService { + + private final PlatformSecurityContext context; + private final JdbcTemplate jdbcTemplate; + private final DatabaseSpecificSQLGenerator sqlGenerator; + + // mappers + private final SavingsAccountTransactionTemplateMapper transactionTemplateMapper; + protected SavingsAccountTransactionsMapper transactionsMapper; + private final SavingsAccountTransactionsForBatchMapper savingsAccountTransactionsForBatchMapper; + private final SavingAccountMapper savingAccountMapper; + private final SavingAccountMapperForInterestPosting savingAccountMapperForInterestPosting; + // private final SavingsAccountAnnualFeeMapper annualFeeMapper; + + // pagination + private final PaginationHelper paginationHelper; + + private final ColumnValidator columnValidator; + private final SavingsAccountAssembler savingAccountAssembler; + + private final SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper; + + public SavingsAccountReadPlatformServiceImpl(final PlatformSecurityContext context, final JdbcTemplate jdbcTemplate, + final SavingsAccountAssembler savingAccountAssembler, PaginationHelper paginationHelper, ColumnValidator columnValidator, + DatabaseSpecificSQLGenerator sqlGenerator, SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper) { + this.context = context; + this.jdbcTemplate = jdbcTemplate; + this.sqlGenerator = sqlGenerator; + this.savingsAccountRepositoryWrapper = savingsAccountRepositoryWrapper; + this.transactionTemplateMapper = new SavingsAccountTransactionTemplateMapper(); + this.transactionsMapper = new SavingsAccountTransactionsMapper(); + this.savingsAccountTransactionsForBatchMapper = new SavingsAccountTransactionsForBatchMapper(); + this.savingAccountMapper = new SavingAccountMapper(); + this.columnValidator = columnValidator; + this.paginationHelper = paginationHelper; + this.savingAccountMapperForInterestPosting = new SavingAccountMapperForInterestPosting(); + this.savingAccountAssembler = savingAccountAssembler; + } + + @Override + public Collection retrieveAllForLookup(final Long clientId) { + + final StringBuilder sqlBuilder = new StringBuilder("select " + this.savingAccountMapper.schema()); + sqlBuilder.append(" where sa.client_id = ? and sa.status_enum = 300 "); + + final Object[] queryParameters = new Object[] { clientId }; + return this.jdbcTemplate.query(sqlBuilder.toString(), this.savingAccountMapper, queryParameters); + } + + @Override + public Collection retrieveActiveForLookup(final Long clientId, DepositAccountType depositAccountType) { + + final StringBuilder sqlBuilder = new StringBuilder("select " + this.savingAccountMapper.schema()); + sqlBuilder.append(" where sa.client_id = ? and sa.status_enum = 300 and sa.deposit_type_enum = ? "); + + final Object[] queryParameters = new Object[] { clientId, depositAccountType.getValue() }; + return this.jdbcTemplate.query(sqlBuilder.toString(), this.savingAccountMapper, queryParameters); + } + + @Override + public Collection retrieveActiveForLookup(final Long clientId, DepositAccountType depositAccountType, + String currencyCode) { + final StringBuilder sqlBuilder = new StringBuilder("select " + this.savingAccountMapper.schema()); + sqlBuilder.append(" where sa.client_id = ? and sa.status_enum = 300 and sa.deposit_type_enum = ? and sa.currency_code = ? "); + + final Object[] queryParameters = new Object[] { clientId, depositAccountType.getValue(), currencyCode }; + return this.jdbcTemplate.query(sqlBuilder.toString(), this.savingAccountMapper, queryParameters); + } + + @Override + public Page retrieveAll(final SearchParameters searchParameters) { + + final AppUser currentUser = this.context.authenticatedUser(); + final String hierarchy = currentUser.getOffice().getHierarchy(); + final String hierarchySearchString = hierarchy + "%"; + + final StringBuilder sqlBuilder = new StringBuilder(200); + sqlBuilder.append("select " + sqlGenerator.calcFoundRows() + " "); + sqlBuilder.append(this.savingAccountMapper.schema()); + + sqlBuilder.append(" join m_office o on o.id = c.office_id"); + sqlBuilder.append(" where o.hierarchy like ?"); + + final Object[] objectArray = new Object[10]; + objectArray[0] = hierarchySearchString; + int arrayPos = 1; + if (searchParameters != null) { + + if (StringUtils.isNotBlank(searchParameters.getStatus())) { + sqlBuilder.append(" and sa.status_enum = ?"); + objectArray[arrayPos] = Integer.parseInt(searchParameters.getStatus()); + arrayPos = arrayPos + 1; + } + + if (StringUtils.isNotBlank(searchParameters.getExternalId())) { + sqlBuilder.append(" and sa.external_id = ?"); + objectArray[arrayPos] = searchParameters.getExternalId(); + arrayPos = arrayPos + 1; + } + if (searchParameters.getOfficeId() != null) { + sqlBuilder.append(" and c.office_id = ?"); + objectArray[arrayPos++] = searchParameters.getOfficeId(); + } + if (StringUtils.isNotBlank(searchParameters.getAccountNo())) { + sqlBuilder.append(" and sa.account_no like ?"); + objectArray[arrayPos++] = "%" + searchParameters.getAccountNo() + "%"; + } + if (StringUtils.isNotBlank(searchParameters.getName())) { + sqlBuilder.append(" and c.display_name like ?"); + objectArray[arrayPos++] = "%" + searchParameters.getName() + "%"; + } + if (searchParameters.getDepositType() != null) { + sqlBuilder.append(" and sa.deposit_type_enum = ?"); + objectArray[arrayPos++] = searchParameters.getDepositType(); + } + 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 = Arrays.copyOf(objectArray, arrayPos); + return this.paginationHelper.fetchPage(this.jdbcTemplate, sqlBuilder.toString(), finalObjectArray, this.savingAccountMapper); + } + + @Override + public SavingsAccountData retrieveOne(final Long accountId) { + + try { + final String sql = "select " + this.savingAccountMapper.schema() + " where sa.id = ?"; + + final SavingsAccountData accountData = this.jdbcTemplate.queryForObject(sql, this.savingAccountMapper, + new Object[] { accountId }); // NOSONAR + + // Load joint holders + if (accountData != null) { + final String jointSql = "SELECT jh.id as id, jh.client_id as clientId, jh.cif_number as cifNumber, " + + "jh.display_name as displayName, jh.share_percentage as sharePercentage, " + + "jh.signing_authority as signingAuthority, jh.role as role, jh.is_primary as isPrimary " + + "FROM m_savings_account_joint_holders jh WHERE jh.savings_account_id = ?"; + final List> jointHolders = this.jdbcTemplate.queryForList(jointSql, accountId); + if (!jointHolders.isEmpty()) { + accountData.setJointHolders(jointHolders); + } + } + + return accountData; + } catch (final EmptyResultDataAccessException e) { + throw new SavingsAccountNotFoundException(accountId, e); + } + } + + @Override + public List retrieveAllTransactionData(final List refNo) throws DataAccessException { + String inSql = String.join(",", Collections.nCopies(refNo.size(), "?")); + String sql = "select " + this.savingsAccountTransactionsForBatchMapper.schema() + " where tr.ref_no in (%s)"; + Object[] params = new Object[refNo.size()]; + int i = 0; + for (String element : refNo) { + params[i] = element; + i++; + } + return this.jdbcTemplate.query(String.format(sql, inSql), this.savingsAccountTransactionsForBatchMapper, params); + } + + @Override + public List retrieveAllSavingsDataForInterestPosting(final boolean backdatedTxnsAllowedTill, final int pageSize, + final Integer status, final Long maxSavingsId) { + LocalDate yesterday = DateUtils.getBusinessLocalDate().minusDays(1); + String sql = "select " + this.savingAccountMapperForInterestPosting.schema() + + "join (select a.id from m_savings_account a where a.id > ? and a.status_enum = ? limit ?) b on b.id = sa.id "; + if (backdatedTxnsAllowedTill) { + sql = sql + + "where (CASE WHEN sa.interest_posted_till_date is not null THEN tr.transaction_date >= sa.interest_posted_till_date ELSE tr.transaction_date >= sa.activatedon_date END) "; + } + + sql = sql + " and (sa.interest_posted_till_date is null or sa.interest_posted_till_date <= ? ) "; + // #audit backward compatibility + sql = sql + " order by sa.id, tr.transaction_date, tr." + CREATED_DATE_DB_FIELD + ", tr.created_date, tr.id"; + + List savingsAccountDataList = this.jdbcTemplate.query(sql, this.savingAccountMapperForInterestPosting, // NOSONAR + new Object[] { maxSavingsId, status, pageSize, yesterday }); + for (SavingsAccountData savingsAccountData : savingsAccountDataList) { + this.savingAccountAssembler.assembleSavings(savingsAccountData); + } + return savingsAccountDataList; + } + + private static final class SavingAccountMapperForInterestPosting implements ResultSetExtractor> { + + private final String schemaSql; + + SavingAccountMapperForInterestPosting() { + final StringBuilder sqlBuilder = new StringBuilder(400); + sqlBuilder.append("sa.id as id, sa.account_no as accountNo, sa.external_id as externalId, "); + sqlBuilder.append("sa.deposit_type_enum as depositType, "); + sqlBuilder.append("c.id as clientId, c.office_id as clientOfficeId, "); + sqlBuilder.append("g.id as groupId, g.office_id as groupOfficeId, "); + sqlBuilder.append("sa.status_enum as statusEnum, "); + sqlBuilder.append("sa.sub_status_enum as subStatusEnum, "); + sqlBuilder.append("sa.submittedon_date as submittedOnDate,"); + sqlBuilder.append("sa.rejectedon_date as rejectedOnDate,"); + sqlBuilder.append("sa.withdrawnon_date as withdrawnOnDate,"); + sqlBuilder.append("sa.approvedon_date as approvedOnDate,"); + sqlBuilder.append("sa.activatedon_date as activatedOnDate,"); + sqlBuilder.append("sa.closedon_date as closedOnDate,"); + sqlBuilder.append( + "sa.currency_code as currencyCode, sa.currency_digits as currencyDigits, sa.currency_multiplesof as inMultiplesOf, "); + sqlBuilder.append("sa.nominal_annual_interest_rate as nominalAnnualInterestRate, "); + sqlBuilder.append("sa.interest_compounding_period_enum as interestCompoundingPeriodType, "); + sqlBuilder.append("sa.interest_posting_period_enum as interestPostingPeriodType, "); + sqlBuilder.append("sa.interest_calculation_type_enum as interestCalculationType, "); + sqlBuilder.append("sa.interest_calculation_days_in_year_type_enum as interestCalculationDaysInYearType, "); + sqlBuilder.append("sa.min_required_opening_balance as minRequiredOpeningBalance, "); + sqlBuilder.append("sa.lockin_period_frequency as lockinPeriodFrequency,"); + sqlBuilder.append("sa.lockin_period_frequency_enum as lockinPeriodFrequencyType, "); + sqlBuilder.append("sa.withdrawal_fee_for_transfer as withdrawalFeeForTransfers, "); + sqlBuilder.append("sa.allow_overdraft as allowOverdraft, "); + sqlBuilder.append("sa.overdraft_limit as overdraftLimit, "); + sqlBuilder.append("sa.nominal_annual_interest_rate_overdraft as nominalAnnualInterestRateOverdraft, "); + sqlBuilder.append("sa.min_overdraft_for_interest_calculation as minOverdraftForInterestCalculation, "); + sqlBuilder.append("sa.total_deposits_derived as totalDeposits, "); + sqlBuilder.append("sa.total_withdrawals_derived as totalWithdrawals, "); + sqlBuilder.append("sa.total_withdrawal_fees_derived as totalWithdrawalFees, "); + sqlBuilder.append("sa.total_annual_fees_derived as totalAnnualFees, "); + sqlBuilder.append("sa.total_interest_earned_derived as totalInterestEarned, "); + sqlBuilder.append("sa.total_interest_posted_derived as totalInterestPosted, "); + sqlBuilder.append("sa.total_overdraft_interest_derived as totalOverdraftInterestDerived, "); + sqlBuilder.append("sa.account_balance_derived as accountBalance, "); + sqlBuilder.append("sa.total_fees_charge_derived as totalFeeCharge, "); + sqlBuilder.append("sa.total_penalty_charge_derived as totalPenaltyCharge, "); + sqlBuilder.append("sa.min_balance_for_interest_calculation as minBalanceForInterestCalculation,"); + sqlBuilder.append("sa.min_required_balance as minRequiredBalance, "); + sqlBuilder.append("sa.enforce_min_required_balance as enforceMinRequiredBalance, "); + sqlBuilder.append("sa.max_allowed_lien_limit as maxAllowedLienLimit, "); + sqlBuilder.append("sa.is_lien_allowed as lienAllowed, "); + sqlBuilder.append("sa.on_hold_funds_derived as onHoldFunds, "); + sqlBuilder.append("sa.withhold_tax as withHoldTax, "); + sqlBuilder.append("sa.total_withhold_tax_derived as totalWithholdTax, "); + sqlBuilder.append("sa.last_interest_calculation_date as lastInterestCalculationDate, "); + sqlBuilder.append("sa.total_savings_amount_on_hold as onHoldAmount, "); + sqlBuilder.append("sa.interest_posted_till_date as interestPostedTillDate, "); + sqlBuilder.append("tg.id as taxGroupId, "); + sqlBuilder.append("(select COALESCE(max(sat.transaction_date),sa.activatedon_date) "); + sqlBuilder.append("from m_savings_account_transaction as sat "); + sqlBuilder.append("where sat.is_reversed = false and sat.is_reversal = false "); + sqlBuilder.append("and sat.transaction_type_enum in (1,2) "); + sqlBuilder.append("and sat.savings_account_id = sa.id) as lastActiveTransactionDate, "); + sqlBuilder.append("sp.id as productId, "); + sqlBuilder.append("sp.is_dormancy_tracking_active as isDormancyTrackingActive, "); + sqlBuilder.append("sp.days_to_inactive as daysToInactive, "); + sqlBuilder.append("sp.days_to_dormancy as daysToDormancy, "); + sqlBuilder.append("sp.days_to_escheat as daysToEscheat, "); + sqlBuilder.append("sp.accounting_type as accountingType, "); + sqlBuilder.append("tr.id as transactionId, tr.transaction_type_enum as transactionType, "); + sqlBuilder.append("tr.transaction_date as transactionDate, tr.amount as transactionAmount,"); + sqlBuilder.append("tr.submitted_on_date as transSubmittedOnDate,tr.cumulative_balance_derived as cumulativeBalance,"); + sqlBuilder.append("tr.running_balance_derived as runningBalance, tr.is_reversed as reversed,"); + sqlBuilder.append("tr.balance_end_date_derived as balanceEndDate, tr.overdraft_amount_derived as overdraftAmount,"); + sqlBuilder.append("tr.is_manual as manualTransaction,tr.office_id as officeId, "); + sqlBuilder.append("pd.payment_type_id as paymentType,pd.account_number as accountNumber,pd.check_number as checkNumber, "); + sqlBuilder.append("pd.receipt_number as receiptNumber, pd.bank_number as bankNumber,pd.routing_code as routingCode, "); + sqlBuilder.append("pt.value as paymentTypeName, "); + sqlBuilder.append("msacpb.amount as paidByAmount, msacpb.id as chargesPaidById, "); + sqlBuilder.append( + "msac.id as chargeId, msac.amount as chargeAmount, msac.charge_time_enum as chargeTimeType, msac.is_penalty as isPenaltyCharge, "); + sqlBuilder.append("txd.id as taxDetailsId, txd.amount as taxAmount, "); + sqlBuilder.append("apm.gl_account_id as glAccountIdForInterestOnSavings, apm1.gl_account_id as glAccountIdForSavingsControl, "); + sqlBuilder.append( + "apm2.gl_account_id as glAccountIdForInterestReceivable,apm3.gl_account_id as glAccountIdForOverdraftPorfolio, "); + sqlBuilder.append("apm4.gl_account_id as glAccountIdForInterestPayable, "); + sqlBuilder.append( + "mtc.id as taxComponentId, mtc.debit_account_id as debitAccountId, mtc.credit_account_id as creditAccountId, mtc.percentage as taxPercentage "); + 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("join m_savings_account_transaction tr on sa.id = tr.savings_account_id "); + sqlBuilder.append("left join m_payment_detail pd on pd.id = tr.payment_detail_id "); + sqlBuilder.append("left join m_payment_type pt on pt.id = pd.payment_type_id "); + sqlBuilder.append("left join m_savings_account_charge_paid_by msacpb on msacpb.savings_account_transaction_id = tr.id "); + sqlBuilder.append("left join m_savings_account_charge msac on msac.id = msacpb.savings_account_charge_id "); + 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_tax_group tg on tg.id = sa.tax_group_id "); + sqlBuilder.append("left join m_savings_account_transaction_tax_details txd on txd.savings_transaction_id = tr.id "); + sqlBuilder.append("left join m_tax_component mtc on mtc.id = txd.tax_component_id "); + sqlBuilder.append( + "left join acc_product_mapping apm on apm.product_type = 2 and apm.product_id = sp.id and apm.financial_account_type=3 "); + sqlBuilder.append( + "left join acc_product_mapping apm1 on apm1.product_type = 2 and apm1.product_id = sp.id and apm1.financial_account_type=2 "); + sqlBuilder.append("left join acc_product_mapping apm2 on apm2.product_id = sp.id and apm2.financial_account_type=18 "); + sqlBuilder.append("left join acc_product_mapping apm3 on apm3.product_id = sp.id and apm3.financial_account_type = 11 "); + sqlBuilder.append("left join acc_product_mapping apm4 on apm4.product_id = sp.id and apm4.financial_account_type = 17 "); + + this.schemaSql = sqlBuilder.toString(); + } + + public String schema() { + return this.schemaSql; + } + + @Override + public List extractData(final ResultSet rs) throws SQLException { + + List savingsAccountDataList = new ArrayList<>(); + HashMap savingsMap = new HashMap<>(); + String currencyCode = null; + Integer currencyDigits = null; + Integer inMultiplesOf = null; + CurrencyData currency = null; + HashMap transMap = new HashMap<>(); + HashMap taxDetails = new HashMap<>(); + HashMap chargeDetails = new HashMap<>(); + SavingsAccountTransactionData savingsAccountTransactionData = null; + SavingsAccountData savingsAccountData = null; + int count = 0; + + while (rs.next()) { + final Long id = rs.getLong("id"); + final Long transactionId = rs.getLong("transactionId"); + final Long taxDetailId = JdbcSupport.getLongDefaultToNullIfZero(rs, "taxDetailsId"); + final Long taxComponentId = JdbcSupport.getLongDefaultToNullIfZero(rs, "taxComponentId"); + final String accountNo = rs.getString("accountNo"); + final Long chargeId = rs.getLong("chargeId"); + + if (!savingsMap.containsValue(id)) { + if (count > 0) { + savingsAccountDataList.add(savingsAccountData); + } + count++; + savingsMap.put("id", id); + + final String externalId = rs.getString("externalId"); + final Integer depositTypeId = rs.getInt("depositType"); + final EnumOptionData depositType = SavingsEnumerations.depositType(depositTypeId); + final Long groupId = JdbcSupport.getLong(rs, "groupId"); + final Long groupOfficeId = JdbcSupport.getLong(rs, "groupOfficeId"); + final GroupGeneralData groupGeneralData = new GroupGeneralData(groupId, groupOfficeId); + + final Long clientId = JdbcSupport.getLong(rs, "clientId"); + final Long clientOfficeId = JdbcSupport.getLong(rs, "clientOfficeId"); + final ClientData clientData = ClientData.createClientForInterestPosting(clientId, clientOfficeId); + + final Long glAccountIdForInterestOnSavings = rs.getLong("glAccountIdForInterestOnSavings"); + final Long glAccountIdForSavingsControl = rs.getLong("glAccountIdForSavingsControl"); + + final Long glAccountIdForOverdraftPorfolio = rs.getLong("glAccountIdForOverdraftPorfolio"); + final Long glAccountIdForInterestReceivable = rs.getLong("glAccountIdForInterestReceivable"); + + final Long glAccountIdForInterestPayable = rs.getLong("glAccountIdForInterestPayable"); + + final Long productId = rs.getLong("productId"); + final Integer accountType = rs.getInt("accountingType"); + final AccountingRuleType accountingRuleType = AccountingRuleType.fromInt(accountType); + final EnumOptionData enumOptionDataForAccounting = new EnumOptionData(accountType.longValue(), + accountingRuleType.getCode(), accountingRuleType.getValue().toString()); + final SavingsProductData savingsProductData = SavingsProductData.createForInterestPosting(productId, + enumOptionDataForAccounting); + + final Integer statusEnum = JdbcSupport.getInteger(rs, "statusEnum"); + final SavingsAccountStatusEnumData status = SavingsEnumerations.status(statusEnum); + final Integer subStatusEnum = JdbcSupport.getInteger(rs, "subStatusEnum"); + final SavingsAccountSubStatusEnumData subStatus = SavingsEnumerations.subStatus(subStatusEnum); + final LocalDate lastActiveTransactionDate = JdbcSupport.getLocalDate(rs, "lastActiveTransactionDate"); + final boolean isDormancyTrackingActive = rs.getBoolean("isDormancyTrackingActive"); + final Integer numDaysToInactive = JdbcSupport.getInteger(rs, "daysToInactive"); + final Integer numDaysToDormancy = JdbcSupport.getInteger(rs, "daysToDormancy"); + final Integer numDaysToEscheat = JdbcSupport.getInteger(rs, "daysToEscheat"); + Integer daysToInactive = null; + Integer daysToDormancy = null; + Integer daysToEscheat = null; + + LocalDate currentDate = DateUtils.getBusinessLocalDate(); + if (isDormancyTrackingActive && statusEnum.equals(SavingsAccountStatusType.ACTIVE.getValue())) { + if (subStatusEnum < SavingsAccountSubStatusEnum.ESCHEAT.getValue()) { + daysToEscheat = DateUtils.getExactDifferenceInDays(currentDate, + lastActiveTransactionDate.plusDays(numDaysToEscheat)); + } + if (subStatusEnum < SavingsAccountSubStatusEnum.DORMANT.getValue()) { + daysToDormancy = DateUtils.getExactDifferenceInDays(currentDate, + lastActiveTransactionDate.plusDays(numDaysToDormancy)); + } + if (subStatusEnum < SavingsAccountSubStatusEnum.INACTIVE.getValue()) { + daysToInactive = DateUtils.getExactDifferenceInDays(currentDate, + lastActiveTransactionDate.plusDays(numDaysToInactive)); + } + } + final LocalDate approvedOnDate = JdbcSupport.getLocalDate(rs, "approvedOnDate"); + final LocalDate withdrawnOnDate = JdbcSupport.getLocalDate(rs, "withdrawnOnDate"); + final LocalDate submittedOnDate = JdbcSupport.getLocalDate(rs, "submittedOnDate"); + final LocalDate activatedOnDate = JdbcSupport.getLocalDate(rs, "activatedOnDate"); + final LocalDate closedOnDate = JdbcSupport.getLocalDate(rs, "closedOnDate"); + final SavingsAccountApplicationTimelineData timeline = new SavingsAccountApplicationTimelineData(submittedOnDate, null, + null, null, null, null, null, null, withdrawnOnDate, null, null, null, approvedOnDate, null, null, null, + activatedOnDate, null, null, null, closedOnDate, null, null, null); + + currencyCode = rs.getString("currencyCode"); + currencyDigits = JdbcSupport.getInteger(rs, "currencyDigits"); + inMultiplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf"); + currency = new CurrencyData(currencyCode, currencyDigits, inMultiplesOf); + + final BigDecimal totalDeposits = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalDeposits"); + final BigDecimal totalWithdrawals = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalWithdrawals"); + final BigDecimal totalWithdrawalFees = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalWithdrawalFees"); + final BigDecimal totalAnnualFees = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalAnnualFees"); + + final BigDecimal totalInterestEarned = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalInterestEarned"); + final BigDecimal totalInterestPosted = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "totalInterestPosted"); + final BigDecimal accountBalance = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "accountBalance"); + final BigDecimal totalFeeCharge = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalFeeCharge"); + final BigDecimal totalPenaltyCharge = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalPenaltyCharge"); + final BigDecimal totalOverdraftInterestDerived = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, + "totalOverdraftInterestDerived"); + final BigDecimal totalWithholdTax = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalWithholdTax"); + final LocalDate interestPostedTillDate = JdbcSupport.getLocalDate(rs, "interestPostedTillDate"); + + final BigDecimal minBalanceForInterestCalculation = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, + "minBalanceForInterestCalculation"); + final BigDecimal onHoldFunds = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "onHoldFunds"); + + final BigDecimal onHoldAmount = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "onHoldAmount"); + + BigDecimal availableBalance = accountBalance; + if (availableBalance != null && onHoldFunds != null) { + + availableBalance = availableBalance.subtract(onHoldFunds); + } + + if (availableBalance != null && onHoldAmount != null) { + + availableBalance = availableBalance.subtract(onHoldAmount); + } + + BigDecimal interestNotPosted = BigDecimal.ZERO; + LocalDate lastInterestCalculationDate = null; + if (totalInterestEarned != null) { + interestNotPosted = totalInterestEarned.subtract(totalInterestPosted).add(totalOverdraftInterestDerived); + lastInterestCalculationDate = JdbcSupport.getLocalDate(rs, "lastInterestCalculationDate"); + } + + final SavingsAccountSummaryData summary = new SavingsAccountSummaryData(currency, totalDeposits, totalWithdrawals, + totalWithdrawalFees, totalAnnualFees, totalInterestEarned, totalInterestPosted, accountBalance, totalFeeCharge, + totalPenaltyCharge, totalOverdraftInterestDerived, totalWithholdTax, interestNotPosted, + lastInterestCalculationDate, availableBalance, interestPostedTillDate); + summary.setPrevInterestPostedTillDate(interestPostedTillDate); + + final boolean withHoldTax = rs.getBoolean("withHoldTax"); + final Long taxGroupId = JdbcSupport.getLongDefaultToNullIfZero(rs, "taxGroupId"); + TaxGroupData taxGroupData = null; + if (taxGroupId != null) { + taxGroupData = TaxGroupData.lookup(taxGroupId, null); + } + + final BigDecimal nominalAnnualInterestRate = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, + "nominalAnnualInterestRate"); + + final EnumOptionData interestCompoundingPeriodType = SavingsEnumerations.compoundingInterestPeriodType( + SavingsCompoundingInterestPeriodType.fromInt(JdbcSupport.getInteger(rs, "interestCompoundingPeriodType"))); + + final EnumOptionData interestPostingPeriodType = SavingsEnumerations.interestPostingPeriodType( + SavingsPostingInterestPeriodType.fromInt(JdbcSupport.getInteger(rs, "interestPostingPeriodType"))); + + final EnumOptionData interestCalculationType = SavingsEnumerations.interestCalculationType( + SavingsInterestCalculationType.fromInt(JdbcSupport.getInteger(rs, "interestCalculationType"))); + + final EnumOptionData interestCalculationDaysInYearType = SavingsEnumerations + .interestCalculationDaysInYearType(SavingsInterestCalculationDaysInYearType + .fromInt(JdbcSupport.getInteger(rs, "interestCalculationDaysInYearType"))); + + final BigDecimal minRequiredOpeningBalance = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, + "minRequiredOpeningBalance"); + + final Integer lockinPeriodFrequency = JdbcSupport.getInteger(rs, "lockinPeriodFrequency"); + EnumOptionData lockinPeriodFrequencyType = null; + final Integer lockinPeriodFrequencyTypeValue = JdbcSupport.getInteger(rs, "lockinPeriodFrequencyType"); + if (lockinPeriodFrequencyTypeValue != null) { + final SavingsPeriodFrequencyType lockinPeriodType = SavingsPeriodFrequencyType + .fromInt(lockinPeriodFrequencyTypeValue); + lockinPeriodFrequencyType = SavingsEnumerations.lockinPeriodFrequencyType(lockinPeriodType); + } + + final boolean withdrawalFeeForTransfers = rs.getBoolean("withdrawalFeeForTransfers"); + + final boolean allowOverdraft = rs.getBoolean("allowOverdraft"); + final BigDecimal overdraftLimit = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "overdraftLimit"); + final BigDecimal nominalAnnualInterestRateOverdraft = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, + "nominalAnnualInterestRateOverdraft"); + final BigDecimal minOverdraftForInterestCalculation = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, + "minOverdraftForInterestCalculation"); + + final BigDecimal minRequiredBalance = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "minRequiredBalance"); + final boolean enforceMinRequiredBalance = rs.getBoolean("enforceMinRequiredBalance"); + final BigDecimal maxAllowedLienLimit = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "maxAllowedLienLimit"); + final boolean lienAllowed = rs.getBoolean("lienAllowed"); + savingsAccountData = SavingsAccountData.instance(id, accountNo, depositType, externalId, null, null, null, null, + productId, null, null, null, status, subStatus, null, timeline, currency, nominalAnnualInterestRate, + interestCompoundingPeriodType, interestPostingPeriodType, interestCalculationType, + interestCalculationDaysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType, + withdrawalFeeForTransfers, summary, allowOverdraft, overdraftLimit, minRequiredBalance, + enforceMinRequiredBalance, maxAllowedLienLimit, lienAllowed, minBalanceForInterestCalculation, onHoldFunds, + nominalAnnualInterestRateOverdraft, minOverdraftForInterestCalculation, withHoldTax, taxGroupData, + lastActiveTransactionDate, isDormancyTrackingActive, daysToInactive, daysToDormancy, daysToEscheat, + onHoldAmount); + + savingsAccountData.setClientData(clientData); + savingsAccountData.setGroupGeneralData(groupGeneralData); + savingsAccountData.setSavingsProduct(savingsProductData); + + savingsAccountData.setGlAccountIdForInterestReceivable(glAccountIdForInterestReceivable); + savingsAccountData.setGlAccountIdForOverdraftPorfolio(glAccountIdForOverdraftPorfolio); + + savingsAccountData.setGlAccountIdForInterestPayable(glAccountIdForInterestPayable); + + savingsAccountData.setGlAccountIdForInterestOnSavings(glAccountIdForInterestOnSavings); + savingsAccountData.setGlAccountIdForSavingsControl(glAccountIdForSavingsControl); + } + + if (!transMap.containsValue(transactionId)) { + + final int transactionTypeInt = JdbcSupport.getInteger(rs, "transactionType"); + final SavingsAccountTransactionEnumData transactionType = SavingsEnumerations.transactionType(transactionTypeInt); + + final LocalDate date = JdbcSupport.getLocalDate(rs, "transactionDate"); + final LocalDate balanceEndDate = JdbcSupport.getLocalDate(rs, "balanceEndDate"); + final LocalDate transSubmittedOnDate = JdbcSupport.getLocalDate(rs, "transSubmittedOnDate"); + final BigDecimal amount = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "transactionAmount"); + final BigDecimal overdraftAmount = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "overdraftAmount"); + final BigDecimal outstandingChargeAmount = null; + final BigDecimal runningBalance = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "runningBalance"); + final boolean reversed = rs.getBoolean("reversed"); + final Long officeId = rs.getLong("officeId"); + final BigDecimal cumulativeBalance = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "cumulativeBalance"); + + final boolean postInterestAsOn = false; + + PaymentDetailData paymentDetailData = null; + if (transactionType.isDepositOrWithdrawal()) { + final Long paymentTypeId = JdbcSupport.getLong(rs, "paymentType"); + if (paymentTypeId != null) { + final String typeName = rs.getString("paymentTypeName"); + final PaymentTypeData paymentTypeData = new PaymentTypeData(paymentTypeId, typeName, null, false, null, null, + false); + paymentDetailData = new PaymentDetailData(id, paymentTypeData, null, null, null, null, null); + } + } + + savingsAccountTransactionData = SavingsAccountTransactionData.create(transactionId, transactionType, paymentDetailData, + id, accountNo, date, currency, amount, outstandingChargeAmount, runningBalance, reversed, transSubmittedOnDate, + postInterestAsOn, cumulativeBalance, balanceEndDate); + savingsAccountTransactionData.setOverdraftAmount(overdraftAmount); + + transMap.put("id", transactionId); + if (savingsAccountData.getOfficeId() == null) { + savingsAccountData.setOfficeId(officeId); + } + + savingsAccountData.setSavingsAccountTransactionData(savingsAccountTransactionData); + } + + if (chargeId != null && !chargeDetails.containsValue(chargeId)) { + final boolean isPenalty = rs.getBoolean("isPenaltyCharge"); + final BigDecimal chargeAmount = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "chargeAmount"); + final Integer chargesTimeType = rs.getInt("chargeTimeType"); + final EnumOptionData enumOptionDataForChargesTimeType = new EnumOptionData(chargesTimeType.longValue(), null, null); + final SavingsAccountChargeData savingsAccountChargeData = new SavingsAccountChargeData(chargeId, chargeAmount, + enumOptionDataForChargesTimeType, isPenalty); + + final Long chargesPaidById = rs.getLong("chargesPaidById"); + final BigDecimal chargesPaid = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "paidByAmount"); + final SavingsAccountChargesPaidByData savingsAccountChargesPaidByData = new SavingsAccountChargesPaidByData( + chargesPaidById, chargesPaid); + savingsAccountChargesPaidByData.setSavingsAccountChargeData(savingsAccountChargeData); + if (savingsAccountChargesPaidByData != null) { + savingsAccountTransactionData.setChargesPaidByData(savingsAccountChargesPaidByData); + } + + chargeDetails.put("id", chargeId); + } + + if (taxDetailId != null && !taxDetails.containsValue(taxDetailId)) { + final BigDecimal amount = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "taxAmount"); + final BigDecimal percentage = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "taxPercentage"); + final Long debitId = rs.getLong("debitAccountId"); + final Long creditId = rs.getLong("creditAccountId"); + final GLAccountData debitAccount = GLAccountData.createFrom(debitId); + final GLAccountData creditAccount = GLAccountData.createFrom(creditId); + + if (taxComponentId != null) { + final TaxComponentData taxComponent = TaxComponentData.createTaxComponent(taxComponentId, percentage, debitAccount, + creditAccount); + savingsAccountTransactionData.setTaxDetails(new TaxDetailsData(taxComponent, amount)); + } + + taxDetails.put("id", taxDetailId); + } + + } + if (savingsAccountData != null) { + savingsAccountDataList.add(savingsAccountData); + } + return savingsAccountDataList; + + } + } + + private static final class SavingAccountMapper implements RowMapper { + + private final String schemaSql; + + SavingAccountMapper() { + final StringBuilder sqlBuilder = new StringBuilder(400); + sqlBuilder.append("sa.id as id, sa.account_no as accountNo, sa.external_id as externalId, "); + sqlBuilder.append("sa.deposit_type_enum as depositType, "); + 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 fieldOfficerId, s.display_name as fieldOfficerName, "); + sqlBuilder.append("sa.status_enum as statusEnum, "); + sqlBuilder.append("sa.sub_status_enum as subStatusEnum, "); + sqlBuilder.append("sa.reason_for_block as reasonForBlock, "); + sqlBuilder.append("sa.submittedon_date as submittedOnDate,"); + sqlBuilder.append("sbu.username as submittedByUsername,"); + sqlBuilder.append("sbu.firstname as submittedByFirstname, sbu.lastname as submittedByLastname,"); + + sqlBuilder.append("sa.rejectedon_date as rejectedOnDate,"); + sqlBuilder.append("rbu.username as rejectedByUsername,"); + sqlBuilder.append("rbu.firstname as rejectedByFirstname, rbu.lastname as rejectedByLastname,"); + + sqlBuilder.append("sa.withdrawnon_date as withdrawnOnDate,"); + sqlBuilder.append("wbu.username as withdrawnByUsername,"); + sqlBuilder.append("wbu.firstname as withdrawnByFirstname, wbu.lastname as withdrawnByLastname,"); + + sqlBuilder.append("sa.approvedon_date as approvedOnDate,"); + sqlBuilder.append("abu.username as approvedByUsername,"); + sqlBuilder.append("abu.firstname as approvedByFirstname, abu.lastname as approvedByLastname,"); + + sqlBuilder.append("sa.activatedon_date as activatedOnDate,"); + sqlBuilder.append("avbu.username as activatedByUsername,"); + sqlBuilder.append("avbu.firstname as activatedByFirstname, avbu.lastname as activatedByLastname,"); + + sqlBuilder.append("sa.closedon_date as closedOnDate,"); + sqlBuilder.append("cbu.username as closedByUsername,"); + sqlBuilder.append("cbu.firstname as closedByFirstname, cbu.lastname as closedByLastname,"); + + sqlBuilder.append( + "sa.currency_code as currencyCode, sa.currency_digits as currencyDigits, 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("sa.nominal_annual_interest_rate as nominalAnnualInterestRate, "); + sqlBuilder.append("sa.interest_compounding_period_enum as interestCompoundingPeriodType, "); + sqlBuilder.append("sa.interest_posting_period_enum as interestPostingPeriodType, "); + sqlBuilder.append("sa.interest_calculation_type_enum as interestCalculationType, "); + sqlBuilder.append("sa.interest_calculation_days_in_year_type_enum as interestCalculationDaysInYearType, "); + sqlBuilder.append("sa.min_required_opening_balance as minRequiredOpeningBalance, "); + sqlBuilder.append("sa.lockin_period_frequency as lockinPeriodFrequency,"); + sqlBuilder.append("sa.lockin_period_frequency_enum as lockinPeriodFrequencyType, "); + + sqlBuilder.append("sa.allow_overdraft as allowOverdraft, "); + sqlBuilder.append("sa.overdraft_limit as overdraftLimit, "); + sqlBuilder.append("sa.nominal_annual_interest_rate_overdraft as nominalAnnualInterestRateOverdraft, "); + sqlBuilder.append("sa.min_overdraft_for_interest_calculation as minOverdraftForInterestCalculation, "); + + sqlBuilder.append("sa.total_deposits_derived as totalDeposits, "); + sqlBuilder.append("sa.total_withdrawals_derived as totalWithdrawals, "); + sqlBuilder.append("sa.total_withdrawal_fees_derived as totalWithdrawalFees, "); + sqlBuilder.append("sa.total_annual_fees_derived as totalAnnualFees, "); + sqlBuilder.append("sa.total_interest_earned_derived as totalInterestEarned, "); + sqlBuilder.append("sa.total_interest_posted_derived as totalInterestPosted, "); + sqlBuilder.append("sa.total_overdraft_interest_derived as totalOverdraftInterestDerived, "); + sqlBuilder.append("sa.account_balance_derived as accountBalance, "); + sqlBuilder.append("sa.total_fees_charge_derived as totalFeeCharge, "); + sqlBuilder.append("sa.total_penalty_charge_derived as totalPenaltyCharge, "); + sqlBuilder.append("sa.min_balance_for_interest_calculation as minBalanceForInterestCalculation,"); + sqlBuilder.append("sa.min_required_balance as minRequiredBalance, "); + sqlBuilder.append("sa.enforce_min_required_balance as enforceMinRequiredBalance, "); + sqlBuilder.append("sa.max_allowed_lien_limit as maxAllowedLienLimit, "); + sqlBuilder.append("sa.is_lien_allowed as lienAllowed, "); + sqlBuilder.append("sa.on_hold_funds_derived as onHoldFunds, "); + sqlBuilder.append("sa.withhold_tax as withHoldTax, "); + sqlBuilder.append("sa.total_withhold_tax_derived as totalWithholdTax, "); + sqlBuilder.append("sa.last_interest_calculation_date as lastInterestCalculationDate, "); + sqlBuilder.append("sa.interest_posted_till_date as interestPostedTillDate, "); + sqlBuilder.append("sa.total_savings_amount_on_hold as onHoldAmount, "); + sqlBuilder.append("sa.withdrawal_fee_for_transfer as withdrawalFeeForTransfers, "); + sqlBuilder.append("tg.id as taxGroupId, tg.name as taxGroupName, "); + sqlBuilder.append("(select COALESCE(max(sat.transaction_date),sa.activatedon_date) "); + sqlBuilder.append("from m_savings_account_transaction as sat "); + sqlBuilder.append("where sat.is_reversed = false and sat.is_reversal = false "); + sqlBuilder.append("and sat.transaction_type_enum in (1,2) "); + sqlBuilder.append("and sat.savings_account_id = sa.id) as lastActiveTransactionDate, "); + sqlBuilder.append("sp.is_dormancy_tracking_active as isDormancyTrackingActive, "); + sqlBuilder.append("sp.days_to_inactive as daysToInactive, "); + sqlBuilder.append("sp.days_to_dormancy as daysToDormancy, "); + sqlBuilder.append("sp.days_to_escheat as daysToEscheat, "); + // CBS extended fields + sqlBuilder.append("sa.cbs_account_type as cbsAccountType, "); + sqlBuilder.append("sa.customer_segment as customerSegment, "); + sqlBuilder.append("sa.account_status as accountStatus, "); + sqlBuilder.append("sa.mode_of_operation as modeOfOperation, "); + sqlBuilder.append("sa.special_category as specialCategory, "); + sqlBuilder.append("sa.weaker_section as weakerSection, "); + sqlBuilder.append("sa.guardian_name as guardianName, "); + sqlBuilder.append("sa.guardian_cif_number as guardianCifNumber, "); + sqlBuilder.append("sa.guardian_relationship as guardianRelationship, "); + sqlBuilder.append("sa.minor_date_of_birth as minorDateOfBirth, "); + sqlBuilder.append("sa.guardian_client_id as guardianClientId, "); + sqlBuilder.append("sa.introducer_client_id as introducerClientId, "); + sqlBuilder.append("sa.introducer_name as introducerName, "); + sqlBuilder.append("sa.introducer_cif_number as introducerCifNumber, "); + sqlBuilder.append("sa.nominee_data as nomineeData, "); + sqlBuilder.append("sa.linked_account_id as linkedAccountId, "); + sqlBuilder.append("sa.linked_account_type as linkedAccountType, "); + sqlBuilder.append("sa.linked_account_no as linkedAccountNo, "); + sqlBuilder.append("sp.balance_swap_option as balanceSwapOption, "); + sqlBuilder.append("sp.msme as msme, "); + sqlBuilder.append("COALESCE(sa.nomination_facility, sp.nomination_facility) as nominationFacility, "); + sqlBuilder.append("COALESCE(sa.auto_debit_facility, sp.auto_debit_facility) as autoDebitFacility, "); + // Facility config fields + sqlBuilder.append("safc.atm_facility as atmFacility, "); + sqlBuilder.append("safc.atm_withdrawal_facility as atmWithdrawalFacility, "); + sqlBuilder.append("safc.upi_facility as upiFacility, "); + sqlBuilder.append("safc.pos_transactions_facility as posTransactionsFacility, "); + sqlBuilder.append("safc.cheque_book as chequeBook, "); + sqlBuilder.append("safc.imps_facility as impsFacility, "); + sqlBuilder.append("safc.sms_alerts as smsAlerts, "); + sqlBuilder.append("safc.bbps_facility as bbpsFacility, "); + sqlBuilder.append("safc.insurance_cover as insuranceCover, "); + sqlBuilder.append("safc.bepg_facility as bepgFacility, "); + sqlBuilder.append("safc.neft_facility as neftFacility, "); + sqlBuilder.append("safc.rtgs_facility as rtgsFacility, "); + sqlBuilder.append("safc.inter_bank_total_cap_facility as interBankTotalCapFacility, "); + sqlBuilder.append("safc.withdrawal_facility as withdrawalFacility, "); + sqlBuilder.append("safc.deposit_facility_kyc as depositFacilityKyc, "); + sqlBuilder.append("safc.virtual_debit_card as virtualDebitCard "); + 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 "); + sqlBuilder.append("left join m_appuser sbu on sbu.id = sa.submittedon_userid "); + sqlBuilder.append("left join m_appuser rbu on rbu.id = sa.rejectedon_userid "); + sqlBuilder.append("left join m_appuser wbu on wbu.id = sa.withdrawnon_userid "); + sqlBuilder.append("left join m_appuser abu on abu.id = sa.approvedon_userid "); + sqlBuilder.append("left join m_appuser avbu on avbu.id = sa.activatedon_userid "); + sqlBuilder.append("left join m_appuser cbu on cbu.id = sa.closedon_userid "); + sqlBuilder.append("left join m_tax_group tg on tg.id = sa.tax_group_id "); + sqlBuilder.append("left join m_savings_account_facility_config safc on safc.savings_account_id = sa.id "); + + this.schemaSql = sqlBuilder.toString(); + } + + public String schema() { + return this.schemaSql; + } + + @Override + public SavingsAccountData 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 Integer depositTypeId = rs.getInt("depositType"); + final EnumOptionData depositType = SavingsEnumerations.depositType(depositTypeId); + + 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 Integer statusEnum = JdbcSupport.getInteger(rs, "statusEnum"); + final SavingsAccountStatusEnumData status = SavingsEnumerations.status(statusEnum); + + final Integer subStatusEnum = JdbcSupport.getInteger(rs, "subStatusEnum"); + final SavingsAccountSubStatusEnumData subStatus = SavingsEnumerations.subStatus(subStatusEnum); + final String reasonForBlock = rs.getString("reasonForBlock"); + + final LocalDate lastActiveTransactionDate = JdbcSupport.getLocalDate(rs, "lastActiveTransactionDate"); + final boolean isDormancyTrackingActive = rs.getBoolean("isDormancyTrackingActive"); + final Integer numDaysToInactive = JdbcSupport.getInteger(rs, "daysToInactive"); + final Integer numDaysToDormancy = JdbcSupport.getInteger(rs, "daysToDormancy"); + final Integer numDaysToEscheat = JdbcSupport.getInteger(rs, "daysToEscheat"); + Integer daysToInactive = null; + Integer daysToDormancy = null; + Integer daysToEscheat = null; + + LocalDate currentDate = DateUtils.getBusinessLocalDate(); + if (isDormancyTrackingActive && statusEnum.equals(SavingsAccountStatusType.ACTIVE.getValue())) { + if (subStatusEnum < SavingsAccountSubStatusEnum.ESCHEAT.getValue()) { + daysToEscheat = DateUtils.getExactDifferenceInDays(currentDate, lastActiveTransactionDate.plusDays(numDaysToEscheat)); + } + if (subStatusEnum < SavingsAccountSubStatusEnum.DORMANT.getValue()) { + daysToDormancy = DateUtils.getExactDifferenceInDays(currentDate, lastActiveTransactionDate.plusDays(numDaysToDormancy)); + } + if (subStatusEnum < SavingsAccountSubStatusEnum.INACTIVE.getValue()) { + daysToInactive = DateUtils.getExactDifferenceInDays(currentDate, lastActiveTransactionDate.plusDays(numDaysToInactive)); + } + } + + final LocalDate submittedOnDate = JdbcSupport.getLocalDate(rs, "submittedOnDate"); + final String submittedByUsername = rs.getString("submittedByUsername"); + final String submittedByFirstname = rs.getString("submittedByFirstname"); + final String submittedByLastname = rs.getString("submittedByLastname"); + + final LocalDate rejectedOnDate = JdbcSupport.getLocalDate(rs, "rejectedOnDate"); + final String rejectedByUsername = rs.getString("rejectedByUsername"); + final String rejectedByFirstname = rs.getString("rejectedByFirstname"); + final String rejectedByLastname = rs.getString("rejectedByLastname"); + + final LocalDate withdrawnOnDate = JdbcSupport.getLocalDate(rs, "withdrawnOnDate"); + final String withdrawnByUsername = rs.getString("withdrawnByUsername"); + final String withdrawnByFirstname = rs.getString("withdrawnByFirstname"); + final String withdrawnByLastname = rs.getString("withdrawnByLastname"); + + final LocalDate approvedOnDate = JdbcSupport.getLocalDate(rs, "approvedOnDate"); + final String approvedByUsername = rs.getString("approvedByUsername"); + final String approvedByFirstname = rs.getString("approvedByFirstname"); + final String approvedByLastname = rs.getString("approvedByLastname"); + + final LocalDate activatedOnDate = JdbcSupport.getLocalDate(rs, "activatedOnDate"); + final String activatedByUsername = rs.getString("activatedByUsername"); + final String activatedByFirstname = rs.getString("activatedByFirstname"); + final String activatedByLastname = rs.getString("activatedByLastname"); + + final LocalDate closedOnDate = JdbcSupport.getLocalDate(rs, "closedOnDate"); + final String closedByUsername = rs.getString("closedByUsername"); + final String closedByFirstname = rs.getString("closedByFirstname"); + final String closedByLastname = rs.getString("closedByLastname"); + + final SavingsAccountApplicationTimelineData timeline = new SavingsAccountApplicationTimelineData(submittedOnDate, + submittedByUsername, submittedByFirstname, submittedByLastname, rejectedOnDate, rejectedByUsername, rejectedByFirstname, + rejectedByLastname, withdrawnOnDate, withdrawnByUsername, withdrawnByFirstname, withdrawnByLastname, approvedOnDate, + approvedByUsername, approvedByFirstname, approvedByLastname, activatedOnDate, activatedByUsername, activatedByFirstname, + activatedByLastname, closedOnDate, closedByUsername, closedByFirstname, closedByLastname); + + final String currencyCode = rs.getString("currencyCode"); + final String currencyName = rs.getString("currencyName"); + final String currencyNameCode = rs.getString("currencyNameCode"); + final String currencyDisplaySymbol = rs.getString("currencyDisplaySymbol"); + final Integer currencyDigits = JdbcSupport.getInteger(rs, "currencyDigits"); + final Integer inMultiplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf"); + final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMultiplesOf, currencyDisplaySymbol, + currencyNameCode); + + final BigDecimal nominalAnnualInterestRate = rs.getBigDecimal("nominalAnnualInterestRate"); + + final EnumOptionData interestCompoundingPeriodType = SavingsEnumerations.compoundingInterestPeriodType( + SavingsCompoundingInterestPeriodType.fromInt(JdbcSupport.getInteger(rs, "interestCompoundingPeriodType"))); + + final EnumOptionData interestPostingPeriodType = SavingsEnumerations.interestPostingPeriodType( + SavingsPostingInterestPeriodType.fromInt(JdbcSupport.getInteger(rs, "interestPostingPeriodType"))); + + final EnumOptionData interestCalculationType = SavingsEnumerations + .interestCalculationType(SavingsInterestCalculationType.fromInt(JdbcSupport.getInteger(rs, "interestCalculationType"))); + + final EnumOptionData interestCalculationDaysInYearType = SavingsEnumerations.interestCalculationDaysInYearType( + SavingsInterestCalculationDaysInYearType.fromInt(JdbcSupport.getInteger(rs, "interestCalculationDaysInYearType"))); + + final BigDecimal minRequiredOpeningBalance = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "minRequiredOpeningBalance"); + + final Integer lockinPeriodFrequency = JdbcSupport.getInteger(rs, "lockinPeriodFrequency"); + EnumOptionData lockinPeriodFrequencyType = null; + final Integer lockinPeriodFrequencyTypeValue = JdbcSupport.getInteger(rs, "lockinPeriodFrequencyType"); + if (lockinPeriodFrequencyTypeValue != null) { + final SavingsPeriodFrequencyType lockinPeriodType = SavingsPeriodFrequencyType.fromInt(lockinPeriodFrequencyTypeValue); + lockinPeriodFrequencyType = SavingsEnumerations.lockinPeriodFrequencyType(lockinPeriodType); + } + + final boolean withdrawalFeeForTransfers = rs.getBoolean("withdrawalFeeForTransfers"); + + final boolean allowOverdraft = rs.getBoolean("allowOverdraft"); + final BigDecimal overdraftLimit = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "overdraftLimit"); + final BigDecimal nominalAnnualInterestRateOverdraft = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, + "nominalAnnualInterestRateOverdraft"); + final BigDecimal minOverdraftForInterestCalculation = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, + "minOverdraftForInterestCalculation"); + + final BigDecimal minRequiredBalance = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "minRequiredBalance"); + final boolean enforceMinRequiredBalance = rs.getBoolean("enforceMinRequiredBalance"); + + final BigDecimal maxAllowedLienLimit = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "maxAllowedLienLimit"); + final boolean lienAllowed = rs.getBoolean("lienAllowed"); + + final BigDecimal totalDeposits = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalDeposits"); + final BigDecimal totalWithdrawals = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalWithdrawals"); + final BigDecimal totalWithdrawalFees = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalWithdrawalFees"); + final BigDecimal totalAnnualFees = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalAnnualFees"); + + final BigDecimal totalInterestEarned = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalInterestEarned"); + final BigDecimal totalInterestPosted = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "totalInterestPosted"); + final BigDecimal accountBalance = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "accountBalance"); + final BigDecimal totalFeeCharge = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalFeeCharge"); + final BigDecimal totalPenaltyCharge = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalPenaltyCharge"); + final BigDecimal totalOverdraftInterestDerived = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, + "totalOverdraftInterestDerived"); + final BigDecimal totalWithholdTax = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalWithholdTax"); + final LocalDate interestPostedTillDate = JdbcSupport.getLocalDate(rs, "interestPostedTillDate"); + + final BigDecimal minBalanceForInterestCalculation = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, + "minBalanceForInterestCalculation"); + final BigDecimal onHoldFunds = rs.getBigDecimal("onHoldFunds"); + + final BigDecimal onHoldAmount = rs.getBigDecimal("onHoldAmount"); + + BigDecimal availableBalance = accountBalance; + if (availableBalance != null && onHoldFunds != null) { + availableBalance = availableBalance.subtract(onHoldFunds); + } + + if (availableBalance != null && onHoldAmount != null) { + + availableBalance = availableBalance.subtract(onHoldAmount); + } + + BigDecimal interestNotPosted = BigDecimal.ZERO; + LocalDate lastInterestCalculationDate = null; + if (totalInterestEarned != null) { + interestNotPosted = totalInterestEarned.subtract(totalInterestPosted).add(totalOverdraftInterestDerived); + lastInterestCalculationDate = JdbcSupport.getLocalDate(rs, "lastInterestCalculationDate"); + } + + final SavingsAccountSummaryData summary = new SavingsAccountSummaryData(currency, totalDeposits, totalWithdrawals, + totalWithdrawalFees, totalAnnualFees, totalInterestEarned, totalInterestPosted, accountBalance, totalFeeCharge, + totalPenaltyCharge, totalOverdraftInterestDerived, totalWithholdTax, interestNotPosted, lastInterestCalculationDate, + availableBalance, interestPostedTillDate); + + final boolean withHoldTax = rs.getBoolean("withHoldTax"); + final Long taxGroupId = JdbcSupport.getLong(rs, "taxGroupId"); + final String taxGroupName = rs.getString("taxGroupName"); + TaxGroupData taxGroupData = null; + if (taxGroupId != null) { + taxGroupData = TaxGroupData.lookup(taxGroupId, taxGroupName); + } + + final SavingsAccountData accountData = SavingsAccountData.instance(id, accountNo, depositType, externalId, groupId, groupName, + clientId, clientName, productId, productName, fieldOfficerId, fieldOfficerName, status, subStatus, reasonForBlock, + timeline, currency, nominalAnnualInterestRate, interestCompoundingPeriodType, interestPostingPeriodType, + interestCalculationType, interestCalculationDaysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency, + lockinPeriodFrequencyType, withdrawalFeeForTransfers, summary, allowOverdraft, overdraftLimit, minRequiredBalance, + enforceMinRequiredBalance, maxAllowedLienLimit, lienAllowed, minBalanceForInterestCalculation, onHoldFunds, + nominalAnnualInterestRateOverdraft, minOverdraftForInterestCalculation, withHoldTax, taxGroupData, + lastActiveTransactionDate, isDormancyTrackingActive, daysToInactive, daysToDormancy, daysToEscheat, onHoldAmount); + + // CBS extended fields + accountData.setCbsAccountType(rs.getString("cbsAccountType")); + accountData.setCustomerSegment(rs.getString("customerSegment")); + accountData.setAccountStatus(rs.getString("accountStatus")); + accountData.setModeOfOperation(rs.getString("modeOfOperation")); + accountData.setSpecialCategory(rs.getString("specialCategory")); + accountData.setWeakerSection(rs.getString("weakerSection")); + accountData.setGuardianName(rs.getString("guardianName")); + accountData.setGuardianCifNumber(rs.getString("guardianCifNumber")); + accountData.setGuardianRelationship(rs.getString("guardianRelationship")); + accountData.setMinorDateOfBirth(JdbcSupport.getLocalDate(rs, "minorDateOfBirth")); + accountData.setGuardianClientId(JdbcSupport.getLong(rs, "guardianClientId")); + accountData.setIntroducerClientId(JdbcSupport.getLong(rs, "introducerClientId")); + accountData.setIntroducerName(rs.getString("introducerName")); + accountData.setIntroducerCifNumber(rs.getString("introducerCifNumber")); + accountData.setLinkedAccountId(JdbcSupport.getLong(rs, "linkedAccountId")); + accountData.setLinkedAccountType(rs.getString("linkedAccountType")); + accountData.setLinkedAccountNo(rs.getString("linkedAccountNo")); + accountData.setBalanceSwapOption(rs.getObject("balanceSwapOption") != null ? rs.getBoolean("balanceSwapOption") : null); + accountData.setMsme(rs.getObject("msme") != null ? rs.getBoolean("msme") : null); + accountData.setNominationFacility(rs.getObject("nominationFacility") != null ? rs.getBoolean("nominationFacility") : null); + accountData.setAutoDebitFacility(rs.getObject("autoDebitFacility") != null ? rs.getBoolean("autoDebitFacility") : null); + + // Nominee as parsed map + final String nomineeDataStr = rs.getString("nomineeData"); + if (nomineeDataStr != null && !nomineeDataStr.isEmpty()) { + try { + com.google.gson.Gson gson = new com.google.gson.Gson(); + @SuppressWarnings("unchecked") + java.util.Map nomineeMap = gson.fromJson(nomineeDataStr, + new com.google.gson.reflect.TypeToken>() {}.getType()); + accountData.setNomineeData(nomineeMap); + } catch (Exception ignored) {} + } + + // Facility config + final boolean hasFacilityConfig = rs.getObject("atmFacility") != null; + if (hasFacilityConfig) { + final java.util.Map facilityConfigMap = new java.util.LinkedHashMap<>(); + facilityConfigMap.put("atmFacility", rs.getBoolean("atmFacility")); + facilityConfigMap.put("atmWithdrawalFacility", rs.getBoolean("atmWithdrawalFacility")); + facilityConfigMap.put("upiFacility", rs.getBoolean("upiFacility")); + facilityConfigMap.put("posTransactionsFacility", rs.getBoolean("posTransactionsFacility")); + facilityConfigMap.put("chequeBook", rs.getBoolean("chequeBook")); + facilityConfigMap.put("impsFacility", rs.getBoolean("impsFacility")); + facilityConfigMap.put("smsAlerts", rs.getBoolean("smsAlerts")); + facilityConfigMap.put("bbpsFacility", rs.getBoolean("bbpsFacility")); + facilityConfigMap.put("insuranceCover", rs.getBoolean("insuranceCover")); + facilityConfigMap.put("bepgFacility", rs.getBoolean("bepgFacility")); + facilityConfigMap.put("neftFacility", rs.getBoolean("neftFacility")); + facilityConfigMap.put("rtgsFacility", rs.getBoolean("rtgsFacility")); + facilityConfigMap.put("interBankTotalCapFacility", rs.getBoolean("interBankTotalCapFacility")); + facilityConfigMap.put("withdrawalFacility", rs.getBoolean("withdrawalFacility")); + facilityConfigMap.put("depositFacilityKyc", rs.getBoolean("depositFacilityKyc")); + facilityConfigMap.put("virtualDebitCard", rs.getBoolean("virtualDebitCard")); + accountData.setAccountFacilityConfig(facilityConfigMap); + } + + return accountData; + } + } + + private static final class SavingAccountMapperForLookup implements RowMapper { + + private final String schemaSql; + + SavingAccountMapperForLookup() { + final StringBuilder sqlBuilder = new StringBuilder(400); + sqlBuilder.append("sa.id as id, sa.account_no as accountNo, "); + sqlBuilder.append("sa.deposit_type_enum as depositType, "); + sqlBuilder.append("sp.id as productId, sp.name as productName, "); + sqlBuilder.append("sa.status_enum as statusEnum "); + + sqlBuilder.append("from m_savings_account sa "); + sqlBuilder.append("join m_savings_product sp ON sa.product_id = sp.id "); + + this.schemaSql = sqlBuilder.toString(); + } + + public String schema() { + return this.schemaSql; + } + + @Override + public SavingsAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { + + final Long id = rs.getLong("id"); + final String accountNo = rs.getString("accountNo"); + final Integer depositTypeId = rs.getInt("depositType"); + final EnumOptionData depositType = SavingsEnumerations.depositType(depositTypeId); + + final Long productId = rs.getLong("productId"); + final String productName = rs.getString("productName"); + + final Integer statusEnum = JdbcSupport.getInteger(rs, "statusEnum"); + final SavingsAccountStatusEnumData status = SavingsEnumerations.status(statusEnum); + + return SavingsAccountData.lookupWithProductDetails(id, accountNo, depositType, productId, productName, status); + } + } + + @Override + public SavingsAccountTransactionData retrieveDepositTransactionTemplate(final Long savingsId, + final DepositAccountType depositAccountType) { + + try { + final String sql = "select " + this.transactionTemplateMapper.schema() + " where sa.id = ? and sa.deposit_type_enum = ?"; + + return this.jdbcTemplate.queryForObject(sql, this.transactionTemplateMapper, // NOSONAR + new Object[] { savingsId, depositAccountType.getValue() }); + } catch (final EmptyResultDataAccessException e) { + throw new SavingsAccountNotFoundException(savingsId, e); + } + } + + @Override + public Collection retrieveAllTransactions(final Long savingsId, DepositAccountType depositAccountType) { + // #audit backward compatibility + final String sql = "select " + this.transactionsMapper.schema() + + " where sa.id = ? and sa.deposit_type_enum = ? order by tr.transaction_date DESC," + " tr." + CREATED_DATE_DB_FIELD + + " DESC, tr.created_date DESC, tr.id DESC"; + return this.jdbcTemplate.query(sql, this.transactionsMapper, new Object[] { savingsId, depositAccountType.getValue() }); // NOSONAR + } + + @Override + public SavingsAccountTransactionData retrieveSavingsTransaction(final Long savingsId, final Long transactionId, + DepositAccountType depositAccountType) { + + final String sql = "select " + this.transactionsMapper.schema() + " where sa.id = ? and sa.deposit_type_enum = ? and tr.id= ?"; + + return this.jdbcTemplate.queryForObject(sql, this.transactionsMapper, // NOSONAR + new Object[] { savingsId, depositAccountType.getValue(), transactionId }); + } + + private static final class SavingsAccountTransactionsForBatchMapper implements RowMapper { + + private final String schemaSql; + + SavingsAccountTransactionsForBatchMapper() { + + final StringBuilder sqlBuilder = new StringBuilder(400); + sqlBuilder.append("tr.id as transactionId, tr.ref_no as refNo "); + sqlBuilder.append("from m_savings_account_transaction tr"); + + this.schemaSql = sqlBuilder.toString(); + } + + public String schema() { + return this.schemaSql; + } + + @Override + public SavingsAccountTransactionData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { + final Long id = rs.getLong("transactionId"); + SavingsAccountTransactionData savingsAccountTransactionData = SavingsAccountTransactionData.create(id); + final String refNo = rs.getString("refNo"); + savingsAccountTransactionData.setRefNo(refNo); + return savingsAccountTransactionData; + } + } + + /* + * @Override public Collection retrieveAccountsWithAnnualFeeDue() { final String sql = + * "select " + this.annualFeeMapper.schema() + + * " where sa.annual_fee_next_due_date is not null and sa.annual_fee_next_due_date <= NOW()" ; + * + * return this.jdbcTemplate.query(sql, this.annualFeeMapper, new Object[] {}); } + */ + + public static class SavingsAccountTransactionsMapper implements RowMapper { + + private static final String SELECT = buildSelect(); + private static final String FROM = buildFrom(); + private static final String SCHEMA = SELECT + FROM; + + public SavingsAccountTransactionsMapper() {} + + protected static String buildSelect() { + return "tr.id as transactionId, tr.transaction_type_enum as transactionType, " + + "tr.transaction_date as transactionDate, tr.amount as transactionAmount, " + + "tr.release_id_of_hold_amount as releaseTransactionId, tr.reason_for_block as reasonForBlock, " + + "tr.submitted_on_date as submittedOnDate, au.username as submittedByUsername, nt.note as transactionNote, " + + "tr.running_balance_derived as runningBalance, tr.is_reversed as reversed, " + + "tr.is_reversal as isReversal, tr.original_transaction_id as originalTransactionId, tr.is_lien_transaction as lienTransaction, " + + "fromtran.id as fromTransferId, fromtran.is_reversed as fromTransferReversed, " + + "fromtran.transaction_date as fromTransferDate, fromtran.amount as fromTransferAmount, " + + "fromtran.description as fromTransferDescription, " + + "totran.id as toTransferId, totran.is_reversed as toTransferReversed, " + + "totran.transaction_date as toTransferDate, totran.amount as toTransferAmount, " + + "totran.description as toTransferDescription, sa.id as savingsId, sa.account_no as accountNo, " + + "pd.payment_type_id as paymentType,pd.account_number as accountNumber,pd.check_number as checkNumber, " + + "pd.receipt_number as receiptNumber, pd.bank_number as bankNumber,pd.routing_code as routingCode, " + + "sa.currency_code as currencyCode, sa.currency_digits as currencyDigits, sa.currency_multiplesof as inMultiplesOf, " + + "curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, " + + "curr.display_symbol as currencyDisplaySymbol, pt.value as paymentTypeName, " + "tr.is_manual as postInterestAsOn "; + } + + protected static String buildFrom() { + return " FROM m_savings_account_transaction tr join m_savings_account sa on tr.savings_account_id = sa.id " + + "join m_currency curr on curr.code = sa.currency_code " + + "left join m_account_transfer_transaction fromtran on fromtran.from_savings_transaction_id = tr.id " + + "left join m_account_transfer_transaction totran on totran.to_savings_transaction_id = tr.id " + + "left join m_payment_detail pd on tr.payment_detail_id = pd.id " + + "left join m_payment_type pt on pd.payment_type_id = pt.id left join m_appuser au on au.id= tr." + CREATED_BY_DB_FIELD + + " left join m_note nt ON nt.savings_account_transaction_id=tr.id "; + } + + public String schema() { + return SCHEMA; + } + + public String select() { + return SELECT; + } + + public String from() { + return FROM; + } + + @Override + public SavingsAccountTransactionData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { + final Long id = rs.getLong("transactionId"); + final int transactionTypeInt = JdbcSupport.getInteger(rs, "transactionType"); + final SavingsAccountTransactionEnumData transactionType = SavingsEnumerations.transactionType(transactionTypeInt); + + final LocalDate date = JdbcSupport.getLocalDate(rs, "transactionDate"); + final LocalDate submittedOnDate = JdbcSupport.getLocalDate(rs, "submittedOnDate"); + final BigDecimal amount = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "transactionAmount"); + final Long releaseTransactionId = rs.getLong("releaseTransactionId"); + final String reasonForBlock = rs.getString("reasonForBlock"); + final BigDecimal outstandingChargeAmount = null; + final BigDecimal runningBalance = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "runningBalance"); + final boolean reversed = rs.getBoolean("reversed"); + final boolean isReversal = rs.getBoolean("isReversal"); + final Long originalTransactionId = rs.getLong("originalTransactionId"); + final Boolean lienTransaction = rs.getBoolean("lienTransaction"); + + final Long savingsId = rs.getLong("savingsId"); + final String accountNo = rs.getString("accountNo"); + final boolean postInterestAsOn = rs.getBoolean("postInterestAsOn"); + + PaymentDetailData paymentDetailData = null; + if (transactionType.isDepositOrWithdrawal()) { + final Long paymentTypeId = JdbcSupport.getLong(rs, "paymentType"); + if (paymentTypeId != null) { + final String typeName = rs.getString("paymentTypeName"); + final PaymentTypeData paymentType = PaymentTypeData.builder().id(paymentTypeId).name(typeName).build(); + final String accountNumber = rs.getString("accountNumber"); + final String checkNumber = rs.getString("checkNumber"); + final String routingCode = rs.getString("routingCode"); + final String receiptNumber = rs.getString("receiptNumber"); + final String bankNumber = rs.getString("bankNumber"); + paymentDetailData = new PaymentDetailData(id, paymentType, accountNumber, checkNumber, routingCode, receiptNumber, + bankNumber); + } + } + + 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); + + AccountTransferData transfer = null; + final Long fromTransferId = JdbcSupport.getLong(rs, "fromTransferId"); + final Long toTransferId = JdbcSupport.getLong(rs, "toTransferId"); + if (fromTransferId != null) { + final LocalDate fromTransferDate = JdbcSupport.getLocalDate(rs, "fromTransferDate"); + final BigDecimal fromTransferAmount = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "fromTransferAmount"); + final boolean fromTransferReversed = rs.getBoolean("fromTransferReversed"); + final String fromTransferDescription = rs.getString("fromTransferDescription"); + + transfer = AccountTransferData.transferBasicDetails(fromTransferId, currency, fromTransferAmount, fromTransferDate, + fromTransferDescription, fromTransferReversed); + } else if (toTransferId != null) { + final LocalDate toTransferDate = JdbcSupport.getLocalDate(rs, "toTransferDate"); + final BigDecimal toTransferAmount = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "toTransferAmount"); + final boolean toTransferReversed = rs.getBoolean("toTransferReversed"); + final String toTransferDescription = rs.getString("toTransferDescription"); + + transfer = AccountTransferData.transferBasicDetails(toTransferId, currency, toTransferAmount, toTransferDate, + toTransferDescription, toTransferReversed); + } + final String submittedByUsername = rs.getString("submittedByUsername"); + final String note = rs.getString("transactionNote"); + return SavingsAccountTransactionData.create(id, transactionType, paymentDetailData, savingsId, accountNo, date, currency, + amount, outstandingChargeAmount, runningBalance, reversed, transfer, submittedOnDate, postInterestAsOn, + submittedByUsername, note, isReversal, originalTransactionId, lienTransaction, releaseTransactionId, reasonForBlock); + } + } + + private static final class SavingsAccountTransactionTemplateMapper implements RowMapper { + + private final String schemaSql; + + SavingsAccountTransactionTemplateMapper() { + final StringBuilder sqlBuilder = new StringBuilder(400); + sqlBuilder.append("sa.id as id, sa.account_no as accountNo, "); + sqlBuilder.append( + "sa.currency_code as currencyCode, sa.currency_digits as currencyDigits, 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("sa.min_required_opening_balance as minRequiredOpeningBalance "); + sqlBuilder.append("from m_savings_account sa "); + sqlBuilder.append("join m_currency curr on curr.code = sa.currency_code "); + + this.schemaSql = sqlBuilder.toString(); + } + + public String schema() { + return this.schemaSql; + } + + @Override + public SavingsAccountTransactionData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { + + final Long savingsId = rs.getLong("id"); + final String accountNo = rs.getString("accountNo"); + + 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 SavingsAccountTransactionData.template(savingsId, accountNo, DateUtils.getBusinessLocalDate(), currency); + } + } + + @Override + public Collection retrieveForLookup(Long clientId, Boolean overdraft) { + + SavingAccountMapperForLookup accountMapperForLookup = new SavingAccountMapperForLookup(); + final StringBuilder sqlBuilder = new StringBuilder("select " + accountMapperForLookup.schema()); + sqlBuilder.append(" where sa.client_id = ? and sa.status_enum = 300"); + Object[] queryParameters = null; + if (overdraft == null) { + queryParameters = new Object[] { clientId }; + } else { + sqlBuilder.append(" and sa.allow_overdraft = ?"); + queryParameters = new Object[] { clientId, overdraft }; + } + return this.jdbcTemplate.query(sqlBuilder.toString(), accountMapperForLookup, queryParameters); + + } + + @Override + public List retrieveSavingsIdsPendingInactive(LocalDate tenantLocalDate) { + List ret = null; + StringBuilder sql = new StringBuilder("select sa.id "); + sql.append(" from m_savings_account as sa "); + sql.append(" inner join m_savings_product as sp on (sa.product_id = sp.id and sp.is_dormancy_tracking_active = true) "); + sql.append(" where sa.status_enum = 300 "); + sql.append(" and sa.sub_status_enum = 0 "); + String compareDate = "(select COALESCE(max(sat.transaction_date), sa.activatedon_date) " + + "from m_savings_account_transaction as sat where sat.is_reversed = false and sat.is_reversal = false" + + " and sat.transaction_type_enum in (1,2) and sat.savings_account_id = sa.id)"; + sql.append(" and ").append(sqlGenerator.dateDiff("?", compareDate)).append(" >= sp.days_to_inactive "); + + try { + ret = this.jdbcTemplate.queryForList(sql.toString(), new Object[] { tenantLocalDate }, Long.class); + } catch (EmptyResultDataAccessException e) { + // ignore empty result scenario + } catch (DataAccessException e) { + throw e; + } + + return ret; + } + + @Override + public List retrieveSavingsIdsPendingDormant(LocalDate tenantLocalDate) { + List ret = null; + StringBuilder sql = new StringBuilder("select sa.id "); + sql.append(" from m_savings_account as sa "); + sql.append(" inner join m_savings_product as sp on (sa.product_id = sp.id and sp.is_dormancy_tracking_active = true) "); + sql.append(" where sa.status_enum = 300 "); + sql.append(" and sa.sub_status_enum = 100 "); + sql.append(" and " + sqlGenerator.dateDiff("?", + "(select COALESCE(max(sat.transaction_date),sa.activatedon_date) from m_savings_account_transaction as sat where sat.is_reversed = false and sat.is_reversal = false and sat.transaction_type_enum in (1,2) and sat.savings_account_id = sa.id)") + + " "); + sql.append(" >= sp.days_to_dormancy "); + + try { + ret = this.jdbcTemplate.queryForList(sql.toString(), new Object[] { tenantLocalDate }, Long.class); + } catch (EmptyResultDataAccessException e) { + // ignore empty result scenario + } catch (DataAccessException e) { + throw e; + } + + return ret; + } + + @Override + public List retrieveSavingsIdsPendingEscheat(LocalDate tenantLocalDate) { + List ret = null; + StringBuilder sql = new StringBuilder("select sa.id "); + sql.append(" from m_savings_account as sa "); + sql.append(" inner join m_savings_product as sp on (sa.product_id = sp.id and sp.is_dormancy_tracking_active = true) "); + sql.append(" where sa.status_enum = 300 "); + sql.append(" and sa.sub_status_enum = 200 "); + sql.append(" and " + sqlGenerator.dateDiff("?", + "(select COALESCE(max(sat.transaction_date),sa.activatedon_date) from m_savings_account_transaction as sat where sat.is_reversed = false and sat.is_reversal = false and sat.transaction_type_enum in (1,2) and sat.savings_account_id = sa.id)") + + " "); + sql.append(" >= sp.days_to_escheat "); + + try { + ret = this.jdbcTemplate.queryForList(sql.toString(), Long.class, new Object[] { tenantLocalDate }); + } catch (EmptyResultDataAccessException e) { + // ignore empty result scenario + } catch (DataAccessException e) { + throw e; + } + + return ret; + } + + @Override + public boolean isAccountBelongsToClient(final Long clientId, final Long accountId, final DepositAccountType depositAccountType, + final String currencyCode) { + try { + final StringBuilder buff = new StringBuilder("select count(*) from m_savings_account sa "); + buff.append( + " where sa.id = ? and sa.client_id = ? and sa.deposit_type_enum = ? and sa.currency_code = ? and sa.status_enum = 300"); + return this.jdbcTemplate.queryForObject(buff.toString(), Integer.class, accountId, clientId, depositAccountType.getValue(), + currencyCode) > 0; + } catch (final EmptyResultDataAccessException e) { + throw new SavingsAccountNotFoundException(accountId, e); + } + } + /* + * private static final class SavingsAccountAnnualFeeMapper implements RowMapper { + * + * private final String schemaSql; + * + * public SavingsAccountAnnualFeeMapper() { final StringBuilder sqlBuilder = new StringBuilder(200); + * sqlBuilder.append("sa.id as id, sa.account_no as accountNo, "); sqlBuilder + * .append("sa.annual_fee_next_due_date as annualFeeNextDueDate "); sqlBuilder.append("from m_savings_account sa "); + * + * this.schemaSql = sqlBuilder.toString(); } + * + * public String schema() { return this.schemaSql; } + * + * @Override public SavingsAccountAnnualFeeData mapRow(final ResultSet rs, + * + * @SuppressWarnings("unused") final int rowNum) throws SQLException { + * + * final Long id = rs.getLong("id"); final String accountNo = rs.getString("accountNo"); final LocalDate + * annualFeeNextDueDate = JdbcSupport.getLocalDate(rs, "annualFeeNextDueDate"); + * + * return SavingsAccountAnnualFeeData.instance(id, accountNo, annualFeeNextDueDate); } } + */ + + @Override + public String retrieveAccountNumberByAccountId(Long accountId) { + try { + final String sql = "select s.account_no from m_savings_account s where s.id = ?"; + return this.jdbcTemplate.queryForObject(sql, String.class, accountId); + } catch (final EmptyResultDataAccessException e) { + throw new SavingsAccountNotFoundException(accountId, e); + } + } + + @Override + public List getAccountsIdsByStatusPaged(Integer status, int pageSize, Long maxSavingsIdInList) { + String sql = new StringBuilder().append(" SELECT sa.id FROM m_savings_account sa ") + .append(" where sa.id > ? and sa.status_enum = ? ").append(" order by sa.id limit ?").toString(); + + try { + return this.jdbcTemplate.queryForList(sql, Long.class, new Object[] { maxSavingsIdInList, status, pageSize }); + } catch (EmptyResultDataAccessException e) { + return new ArrayList<>(); + } + } + + @Override + public Long retrieveAccountIdByExternalId(final ExternalId externalId) { + return savingsAccountRepositoryWrapper.findIdByExternalId(externalId); + } + + @Override + public List retrievePeriodicAccrualData(LocalDate tillDate, SavingsAccount savings) { + Long savingsId = (savings != null) ? savings.getId() : null; + Integer status = SavingsAccountStatusType.ACTIVE.getValue(); + Integer accountingRule = AccountingRuleType.ACCRUAL_PERIODIC.getValue(); + + return this.savingsAccountRepositoryWrapper.findAccrualData(tillDate, savingsId, status, accountingRule); + } +} From 9f627bde6116e5e2e3a45027a0bbc59cdc77db81 Mon Sep 17 00:00:00 2001 From: Jaanu423 Date: Thu, 21 May 2026 13:56:31 +0530 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20accounts=20list=20=E2=80=94=20activ?= =?UTF-8?q?e-only=20filter,=20date=20range=20&=20as-on-date=20search=20(ba?= =?UTF-8?q?ckend)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SearchParameters: added fromDate, toDate, asOnDate (LocalDate) fields with hasX() helpers - SavingsAccountsApiResource: added status, fromDate, toDate, asOnDate query params - SavingsAccountReadPlatformServiceImpl: resolveStatusEnum(), date filters on activatedon_date, default ORDER BY id DESC - CurrentAccountsApiResource + service: same filters on ca.status_enum and ca.activatedon_date - DepositAccountReadPlatformService (interface): new retrieveAllPaged overload with date/status params - DepositAccountReadPlatformServiceImpl: resolveStatusEnum(), date filters, default ORDER BY id DESC for FD/RD - FixedDepositAccountsApiResource + RecurringDepositAccountsApiResource: pass new params to service Co-Authored-By: Claude Sonnet 4.6 --- .../core/service/SearchParameters.java | 16 +++++ .../CurrentAccountReadPlatformService.java | 4 ++ .../api/CurrentAccountsApiResource.java | 13 ++++- .../api/FixedDepositAccountsApiResource.java | 13 ++++- .../RecurringDepositAccountsApiResource.java | 13 ++++- .../api/SavingsAccountsApiResource.java | 14 ++++- ...CurrentAccountReadPlatformServiceImpl.java | 58 +++++++++++++++++++ .../DepositAccountReadPlatformService.java | 4 ++ ...DepositAccountReadPlatformServiceImpl.java | 56 +++++++++++++++++- ...SavingsAccountReadPlatformServiceImpl.java | 45 ++++++++++++-- 10 files changed, 223 insertions(+), 13 deletions(-) diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/SearchParameters.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/SearchParameters.java index e9b83b854..e8e563ccf 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/SearchParameters.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/SearchParameters.java @@ -18,6 +18,7 @@ */ package org.apache.fineract.infrastructure.core.service; +import java.time.LocalDate; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; @@ -58,6 +59,21 @@ public class SearchParameters { @Getter(AccessLevel.NONE) private Boolean isSelfUser; private Integer legalForm; + private LocalDate fromDate; + private LocalDate toDate; + private LocalDate asOnDate; + + public boolean hasFromDate() { + return this.fromDate != null; + } + + public boolean hasToDate() { + return this.toDate != null; + } + + public boolean hasAsOnDate() { + return this.asOnDate != null; + } public Integer getLimit() { if (limit == null) { diff --git a/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentaccount/service/CurrentAccountReadPlatformService.java b/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentaccount/service/CurrentAccountReadPlatformService.java index bda1ddc5b..a350be55d 100644 --- a/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentaccount/service/CurrentAccountReadPlatformService.java +++ b/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentaccount/service/CurrentAccountReadPlatformService.java @@ -18,6 +18,7 @@ */ package org.apache.fineract.portfolio.currentaccount.service; +import java.time.LocalDate; import java.util.List; import java.util.Map; @@ -28,4 +29,7 @@ public interface CurrentAccountReadPlatformService { List> retrieveAll(); List> retrieveAll(Integer offset, Integer limit, String accountNo, String clientName); + + List> retrieveAll(Integer offset, Integer limit, String accountNo, String clientName, + String status, LocalDate fromDate, LocalDate toDate, LocalDate asOnDate); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/CurrentAccountsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/CurrentAccountsApiResource.java index 6da4821dc..ccaf3ff11 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/CurrentAccountsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/CurrentAccountsApiResource.java @@ -38,6 +38,7 @@ import jakarta.ws.rs.core.UriInfo; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonParser; +import java.time.LocalDate; import java.util.List; import java.util.Map; import lombok.RequiredArgsConstructor; @@ -103,10 +104,18 @@ public class CurrentAccountsApiResource { @QueryParam("orderBy") final String orderBy, @QueryParam("sortOrder") final String sortOrder, @QueryParam("accountNo") @Parameter(description = "accountNo") final String accountNo, - @QueryParam("clientName") @Parameter(description = "clientName") final String clientName) { + @QueryParam("clientName") @Parameter(description = "clientName") final String clientName, + @QueryParam("status") @Parameter(description = "status") final String status, + @QueryParam("fromDate") @Parameter(description = "fromDate (YYYY-MM-DD)") final String fromDate, + @QueryParam("toDate") @Parameter(description = "toDate (YYYY-MM-DD)") final String toDate, + @QueryParam("asOnDate") @Parameter(description = "asOnDate (YYYY-MM-DD)") final String asOnDate) { context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.CURRENT_ACCOUNT_RESOURCE_NAME); - final List> accounts = currentAccountReadPlatformService.retrieveAll(offset, limit, accountNo, clientName); + final List> accounts = currentAccountReadPlatformService.retrieveAll(offset, limit, accountNo, clientName, + status, + fromDate != null ? LocalDate.parse(fromDate) : null, + toDate != null ? LocalDate.parse(toDate) : null, + asOnDate != null ? LocalDate.parse(asOnDate) : null); return GSON.toJson(accounts); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/FixedDepositAccountsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/FixedDepositAccountsApiResource.java index ab67363e6..7a6946884 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/FixedDepositAccountsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/FixedDepositAccountsApiResource.java @@ -43,6 +43,7 @@ import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.UriInfo; import java.io.InputStream; import java.math.BigDecimal; +import java.time.LocalDate; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; @@ -161,7 +162,11 @@ public class FixedDepositAccountsApiResource { @QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy, @QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder, @QueryParam("accountNo") @Parameter(description = "accountNo") final String accountNo, - @QueryParam("clientName") @Parameter(description = "clientName") final String clientName) { + @QueryParam("clientName") @Parameter(description = "clientName") final String clientName, + @QueryParam("status") @Parameter(description = "status") final String status, + @QueryParam("fromDate") @Parameter(description = "fromDate (YYYY-MM-DD)") final String fromDate, + @QueryParam("toDate") @Parameter(description = "toDate (YYYY-MM-DD)") final String toDate, + @QueryParam("asOnDate") @Parameter(description = "asOnDate (YYYY-MM-DD)") final String asOnDate) { this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.FIXED_DEPOSIT_ACCOUNT_RESOURCE_NAME); sqlValidator.validate(orderBy); @@ -172,7 +177,11 @@ public class FixedDepositAccountsApiResource { if (paginationParameters.isPaged()) { final Page account = this.depositAccountReadPlatformService - .retrieveAllPaged(DepositAccountType.FIXED_DEPOSIT, paginationParameters, accountNo, clientName); + .retrieveAllPaged(DepositAccountType.FIXED_DEPOSIT, paginationParameters, accountNo, clientName, + status, + fromDate != null ? LocalDate.parse(fromDate) : null, + toDate != null ? LocalDate.parse(toDate) : null, + asOnDate != null ? LocalDate.parse(asOnDate) : null); return this.toApiJsonSerializer.serialize(settings, account, DepositsApiConstants.FIXED_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/RecurringDepositAccountsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/RecurringDepositAccountsApiResource.java index 0ed66a8ff..a8c66f90d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/RecurringDepositAccountsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/RecurringDepositAccountsApiResource.java @@ -43,6 +43,7 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.UriInfo; import java.io.InputStream; +import java.time.LocalDate; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; @@ -142,7 +143,11 @@ public class RecurringDepositAccountsApiResource { @QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy, @QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder, @QueryParam("accountNo") @Parameter(description = "accountNo") final String accountNo, - @QueryParam("clientName") @Parameter(description = "clientName") final String clientName) { + @QueryParam("clientName") @Parameter(description = "clientName") final String clientName, + @QueryParam("status") @Parameter(description = "status") final String status, + @QueryParam("fromDate") @Parameter(description = "fromDate (YYYY-MM-DD)") final String fromDate, + @QueryParam("toDate") @Parameter(description = "toDate (YYYY-MM-DD)") final String toDate, + @QueryParam("asOnDate") @Parameter(description = "asOnDate (YYYY-MM-DD)") final String asOnDate) { this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME); sqlValidator.validate(orderBy); @@ -153,7 +158,11 @@ public class RecurringDepositAccountsApiResource { if (paginationParameters.isPaged()) { final Page account = this.depositAccountReadPlatformService - .retrieveAllPaged(DepositAccountType.RECURRING_DEPOSIT, paginationParameters, accountNo, clientName); + .retrieveAllPaged(DepositAccountType.RECURRING_DEPOSIT, paginationParameters, accountNo, clientName, + status, + fromDate != null ? LocalDate.parse(fromDate) : null, + toDate != null ? LocalDate.parse(toDate) : null, + asOnDate != null ? LocalDate.parse(asOnDate) : null); return this.toApiJsonSerializer.serialize(settings, account, DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountsApiResource.java index 2107cba03..16b8d270c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountsApiResource.java @@ -40,6 +40,7 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.UriInfo; import java.io.InputStream; +import java.time.LocalDate; import java.util.Arrays; import java.util.Collection; import java.util.Set; @@ -133,7 +134,11 @@ public class SavingsAccountsApiResource { @QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder, @QueryParam("accountNo") @Parameter(description = "accountNo") final String accountNo, @QueryParam("clientName") @Parameter(description = "clientName") final String clientName, - @QueryParam("depositType") @Parameter(description = "depositType") final Integer depositType) { + @QueryParam("depositType") @Parameter(description = "depositType") final Integer depositType, + @QueryParam("status") @Parameter(description = "status") final String status, + @QueryParam("fromDate") @Parameter(description = "fromDate (YYYY-MM-DD)") final String fromDate, + @QueryParam("toDate") @Parameter(description = "toDate (YYYY-MM-DD)") final String toDate, + @QueryParam("asOnDate") @Parameter(description = "asOnDate (YYYY-MM-DD)") final String asOnDate) { context.authenticatedUser().validateHasReadPermission(SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME); @@ -141,7 +146,12 @@ public class SavingsAccountsApiResource { sqlValidator.validate(sortOrder); sqlValidator.validate(externalId); final SearchParameters searchParameters = SearchParameters.builder().limit(limit).externalId(externalId).offset(offset) - .orderBy(orderBy).sortOrder(sortOrder).accountNo(accountNo).name(clientName).depositType(depositType).build(); + .orderBy(orderBy).sortOrder(sortOrder).accountNo(accountNo).name(clientName).depositType(depositType) + .status(status) + .fromDate(fromDate != null ? LocalDate.parse(fromDate) : null) + .toDate(toDate != null ? LocalDate.parse(toDate) : null) + .asOnDate(asOnDate != null ? LocalDate.parse(asOnDate) : null) + .build(); final Page products = savingsAccountReadPlatformService.retrieveAll(searchParameters); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/CurrentAccountReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/CurrentAccountReadPlatformServiceImpl.java index 318875b1d..6491836a9 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/CurrentAccountReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/CurrentAccountReadPlatformServiceImpl.java @@ -22,6 +22,7 @@ import java.math.BigDecimal; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; +import java.time.LocalDate; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; @@ -230,6 +231,13 @@ public class CurrentAccountReadPlatformServiceImpl implements CurrentAccountRead @Override public List> retrieveAll(final Integer offset, final Integer limit, final String accountNo, final String clientName) { + return retrieveAll(offset, limit, accountNo, clientName, null, null, null, null); + } + + @Override + public List> retrieveAll(final Integer offset, final Integer limit, final String accountNo, + final String clientName, final String status, final LocalDate fromDate, final LocalDate toDate, + final LocalDate asOnDate) { final StringBuilder sqlBuilder = new StringBuilder(BASE_SQL); final List params = new ArrayList<>(); boolean hasWhere = false; @@ -246,6 +254,35 @@ public class CurrentAccountReadPlatformServiceImpl implements CurrentAccountRead params.add("%" + clientName + "%"); hasWhere = true; } + if (StringUtils.isNotBlank(status)) { + final Integer statusEnum = resolveStatusEnum(status); + if (statusEnum != null) { + sqlBuilder.append(hasWhere ? " AND " : " WHERE "); + sqlBuilder.append("ca.status_enum = ?"); + params.add(statusEnum); + hasWhere = true; + } + } + if (fromDate != null) { + sqlBuilder.append(hasWhere ? " AND " : " WHERE "); + sqlBuilder.append("ca.activatedon_date >= ?"); + params.add(fromDate); + hasWhere = true; + } + if (toDate != null) { + sqlBuilder.append(hasWhere ? " AND " : " WHERE "); + sqlBuilder.append("ca.activatedon_date <= ?"); + params.add(toDate); + hasWhere = true; + } + if (asOnDate != null) { + sqlBuilder.append(hasWhere ? " AND " : " WHERE "); + sqlBuilder.append("ca.activatedon_date <= ?"); + params.add(asOnDate); + } + + sqlBuilder.append(" ORDER BY ca.id DESC"); + if (limit != null && limit > 0) { sqlBuilder.append(" LIMIT ?"); params.add(limit); @@ -266,6 +303,27 @@ public class CurrentAccountReadPlatformServiceImpl implements CurrentAccountRead return result; } + private Integer resolveStatusEnum(final String status) { + if (status == null) { + return null; + } + return switch (status.toLowerCase()) { + case "active" -> 300; + case "approved" -> 200; + case "submitted", "submittedandpendingapproval" -> 100; + case "withdrawn" -> 400; + case "rejected" -> 500; + case "closed" -> 600; + default -> { + try { + yield Integer.parseInt(status); + } catch (NumberFormatException e) { + yield null; + } + } + }; + } + private Map buildStructuredResponse(Map row) { Map out = new LinkedHashMap<>(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountReadPlatformService.java index 69355d32f..21841f4bd 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountReadPlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountReadPlatformService.java @@ -18,6 +18,7 @@ */ package org.apache.fineract.portfolio.savings.service; +import java.time.LocalDate; import java.util.Collection; import java.util.Map; import org.apache.fineract.infrastructure.core.data.PaginationParameters; @@ -36,6 +37,9 @@ public interface DepositAccountReadPlatformService { Page retrieveAllPaged(DepositAccountType depositAccountType, PaginationParameters paginationParameters, String accountNo, String clientName); + Page retrieveAllPaged(DepositAccountType depositAccountType, PaginationParameters paginationParameters, + String accountNo, String clientName, String status, LocalDate fromDate, LocalDate toDate, LocalDate asOnDate); + Collection retrieveAllForLookup(DepositAccountType depositAccountType); DepositAccountData retrieveOne(DepositAccountType depositAccountType, Long accountId); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountReadPlatformServiceImpl.java index bd466b09e..4e5f8051e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountReadPlatformServiceImpl.java @@ -187,6 +187,13 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead @Override public Page retrieveAllPaged(final DepositAccountType depositAccountType, final PaginationParameters paginationParameters, final String accountNo, final String clientName) { + return retrieveAllPaged(depositAccountType, paginationParameters, accountNo, clientName, null, null, null, null); + } + + @Override + public Page retrieveAllPaged(final DepositAccountType depositAccountType, + final PaginationParameters paginationParameters, final String accountNo, final String clientName, + final String status, final LocalDate fromDate, final LocalDate toDate, final LocalDate asOnDate) { this.paginationParametersDataValidator.validateParameterValues(paginationParameters, supportedOrderByValues, depositAccountType.resourceName()); @@ -203,6 +210,7 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead final List params = new ArrayList<>(); params.add(depositAccountType.getValue()); + if (StringUtils.isNotBlank(accountNo)) { sqlBuilder.append(" and sa.account_no like ? "); params.add("%" + accountNo + "%"); @@ -211,11 +219,57 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead sqlBuilder.append(" and c.display_name like ? "); params.add("%" + clientName + "%"); } - sqlBuilder.append(paginationParameters.paginationSql()); + if (StringUtils.isNotBlank(status)) { + final Integer statusEnum = resolveStatusEnum(status); + if (statusEnum != null) { + sqlBuilder.append(" and sa.status_enum = ? "); + params.add(statusEnum); + } + } + if (fromDate != null) { + sqlBuilder.append(" and sa.activatedon_date >= ? "); + params.add(fromDate); + } + if (toDate != null) { + sqlBuilder.append(" and sa.activatedon_date <= ? "); + params.add(toDate); + } + if (asOnDate != null) { + sqlBuilder.append(" and sa.activatedon_date <= ? "); + params.add(asOnDate); + } + + if (paginationParameters.hasOrderBy()) { + sqlBuilder.append(paginationParameters.paginationSql()); + } else { + sqlBuilder.append(" order by sa.id desc "); + sqlBuilder.append(paginationParameters.limitSql()); + } return this.paginationHelper.fetchPage(this.jdbcTemplate, sqlBuilder.toString(), params.toArray(), depositAccountMapper); } + private Integer resolveStatusEnum(final String status) { + if (status == null) { + return null; + } + return switch (status.toLowerCase()) { + case "active" -> 300; + case "approved" -> 200; + case "submitted", "submittedandpendingapproval" -> 100; + case "withdrawn" -> 400; + case "rejected" -> 500; + case "closed" -> 600; + default -> { + try { + yield Integer.parseInt(status); + } catch (NumberFormatException e) { + yield null; + } + } + }; + } + @Override public Collection retrieveAllForLookup(final DepositAccountType depositAccountType) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java index f4453fbf2..3a38bcc34 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java @@ -166,15 +166,17 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead sqlBuilder.append(" join m_office o on o.id = c.office_id"); sqlBuilder.append(" where o.hierarchy like ?"); - final Object[] objectArray = new Object[10]; + final Object[] objectArray = new Object[15]; objectArray[0] = hierarchySearchString; int arrayPos = 1; if (searchParameters != null) { if (StringUtils.isNotBlank(searchParameters.getStatus())) { - sqlBuilder.append(" and sa.status_enum = ?"); - objectArray[arrayPos] = Integer.parseInt(searchParameters.getStatus()); - arrayPos = arrayPos + 1; + final Integer statusEnum = resolveStatusEnum(searchParameters.getStatus()); + if (statusEnum != null) { + sqlBuilder.append(" and sa.status_enum = ?"); + objectArray[arrayPos++] = statusEnum; + } } if (StringUtils.isNotBlank(searchParameters.getExternalId())) { @@ -198,6 +200,18 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead sqlBuilder.append(" and sa.deposit_type_enum = ?"); objectArray[arrayPos++] = searchParameters.getDepositType(); } + if (searchParameters.hasFromDate()) { + sqlBuilder.append(" and sa.activatedon_date >= ?"); + objectArray[arrayPos++] = searchParameters.getFromDate(); + } + if (searchParameters.hasToDate()) { + sqlBuilder.append(" and sa.activatedon_date <= ?"); + objectArray[arrayPos++] = searchParameters.getToDate(); + } + if (searchParameters.hasAsOnDate()) { + sqlBuilder.append(" and sa.activatedon_date <= ?"); + objectArray[arrayPos++] = searchParameters.getAsOnDate(); + } if (searchParameters.hasOrderBy()) { sqlBuilder.append(" order by ").append(searchParameters.getOrderBy()); this.columnValidator.validateSqlInjection(sqlBuilder.toString(), searchParameters.getOrderBy()); @@ -206,6 +220,8 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead sqlBuilder.append(' ').append(searchParameters.getSortOrder()); this.columnValidator.validateSqlInjection(sqlBuilder.toString(), searchParameters.getSortOrder()); } + } else { + sqlBuilder.append(" order by sa.id desc"); } if (searchParameters.hasLimit()) { @@ -221,6 +237,27 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead return this.paginationHelper.fetchPage(this.jdbcTemplate, sqlBuilder.toString(), finalObjectArray, this.savingAccountMapper); } + private Integer resolveStatusEnum(final String status) { + if (status == null) { + return null; + } + return switch (status.toLowerCase()) { + case "active" -> 300; + case "approved" -> 200; + case "submitted", "submittedandpendingapproval" -> 100; + case "withdrawn" -> 400; + case "rejected" -> 500; + case "closed" -> 600; + default -> { + try { + yield Integer.parseInt(status); + } catch (NumberFormatException e) { + yield null; + } + } + }; + } + @Override public SavingsAccountData retrieveOne(final Long accountId) {