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; } +}