diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/glaccount/data/GLAccountData.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/glaccount/data/GLAccountData.java index 0859cb2eb..ef3d82a15 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/glaccount/data/GLAccountData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/glaccount/data/GLAccountData.java @@ -69,6 +69,39 @@ public class GLAccountData implements Serializable { return new GLAccountData(name, parentId, glCode, manualEntriesAllowed, type, usage, description, tagId, rowIndex); } + public static GLAccountData createFrom(final Long id) { + return new GLAccountData(id); + } + + private GLAccountData(final Long id) { + + this.name = null; + this.parentId = null; + this.glCode = null; + this.manualEntriesAllowed = null; + this.type = null; + this.usage = null; + this.description = null; + this.tagId = null; + this.rowIndex = null; + this.id = id; + this.disabled = null; + this.nameDecorated = null; + this.organizationRunningBalance = null; + this.accountTypeOptions = null; + this.usageOptions = null; + this.assetHeaderAccountOptions = null; + this.liabilityHeaderAccountOptions = null; + this.equityHeaderAccountOptions = null; + this.incomeHeaderAccountOptions = null; + this.expenseHeaderAccountOptions = null; + this.allowedAssetsTagOptions = null; + this.allowedLiabilitiesTagOptions = null; + this.allowedEquityTagOptions = null; + this.allowedIncomeTagOptions = null; + this.allowedExpensesTagOptions = null; + } + private GLAccountData(String name, Long parentId, String glCode, Boolean manualEntriesAllowed, EnumOptionData type, EnumOptionData usage, String description, CodeValueData tagId, Integer rowIndex) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/JournalEntryData.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/JournalEntryData.java index 42cb2ddf7..3fe017943 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/JournalEntryData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/JournalEntryData.java @@ -90,6 +90,7 @@ public class JournalEntryData { private String routingCode; private String receiptNumber; private String bankNumber; + private transient Long savingTransactionId; // for opening bal bulk import public JournalEntryData(Long officeId, LocalDate transactionDate, String currencyCode, List credits, @@ -131,7 +132,6 @@ public class JournalEntryData { this.organizationRunningBalance = null; this.runningBalanceComputed = null; this.transactionDetails = null; - } public static JournalEntryData importInstance(Long officeId, LocalDate transactionDate, String currencyCode, Long paymentTypeId, @@ -240,6 +240,40 @@ public class JournalEntryData { this.currency = currency; } + public JournalEntryData(final Long id, final Long officeId, 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 LocalDate createdDate, final String currencyCode, + final Long savingTransactionId) { + this.id = id; + this.officeId = officeId; + this.officeName = null; + 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.savingTransactionId = savingTransactionId; + this.manualEntry = manualEntry; + this.entityType = entityType; + this.entityId = entityId; + this.createdByUserId = null; + this.createdDate = createdDate; + this.createdByUserName = null; + this.comments = null; + this.reversed = false; + this.referenceNumber = null; + this.officeRunningBalance = null; + this.organizationRunningBalance = null; + this.runningBalanceComputed = null; + this.transactionDetails = null; + this.currency = null; + this.currencyCode = currencyCode; + } + public static JournalEntryData fromGLAccountData(final GLAccountData glAccountData) { final Long id = null; @@ -300,4 +334,33 @@ public class JournalEntryData { public String getTransactionId() { return transactionId; } + + public Long getSavingTransactionId() { + return this.savingTransactionId; + } + + public String getCurrencyCode() { + return this.currencyCode; + } + + public boolean isManualEntry() { + return this.manualEntry; + } + + public EnumOptionData getEntityType() { + return this.entityType; + } + + public Long getEntityId() { + return this.entityId; + } + + public LocalDate getCreatedDate() { + return this.createdDate; + } + + public Long getPaymentTypeId() { + return this.paymentTypeId; + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/domain/JournalEntry.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/domain/JournalEntry.java index 3aab4a19a..78f82ce14 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/domain/JournalEntry.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/domain/JournalEntry.java @@ -222,4 +222,12 @@ public class JournalEntry extends AbstractAuditableCustom { return this.shareTransactionId; } + public boolean isReversed() { + return this.reversed; + } + + public String getDescription() { + return this.description; + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorForSavings.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorForSavings.java index dbaf6e170..8816d58f0 100755 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorForSavings.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorForSavings.java @@ -23,5 +23,4 @@ import org.apache.fineract.accounting.journalentry.data.SavingsDTO; public interface AccountingProcessorForSavings { void createJournalEntriesForSavings(SavingsDTO savingsDTO); - } 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 026fdddf5..03e524725 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 @@ -26,6 +26,7 @@ import java.util.Date; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import javax.sql.DataSource; import org.apache.commons.lang3.StringUtils; import org.apache.fineract.accounting.closure.domain.GLClosure; import org.apache.fineract.accounting.closure.domain.GLClosureRepository; @@ -59,6 +60,7 @@ import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGL 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.RoutingDataSource; import org.apache.fineract.organisation.monetary.data.CurrencyData; import org.apache.fineract.organisation.office.domain.Office; import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper; @@ -75,7 +77,11 @@ import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionEnumD import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction; import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransactionRepository; import org.apache.fineract.portfolio.shareaccounts.data.ShareAccountTransactionEnumData; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; @Service @@ -97,6 +103,9 @@ public class AccountingProcessorHelper { private final SavingsAccountTransactionRepository savingsAccountTransactionRepository; private final AccountTransfersReadPlatformService accountTransfersReadPlatformService; private final ChargeRepositoryWrapper chargeRepositoryWrapper; + private final JdbcTemplate jdbcTemplate; + private final DataSource dataSource; + private static final Logger LOG = LoggerFactory.getLogger(AccountingProcessorHelper.class); @Autowired public AccountingProcessorHelper(final JournalEntryRepository glJournalEntryRepository, @@ -107,8 +116,9 @@ public class AccountingProcessorHelper { final AccountTransfersReadPlatformService accountTransfersReadPlatformService, final GLAccountRepositoryWrapper accountRepositoryWrapper, final ClientTransactionRepositoryWrapper clientTransactionRepositoryWrapper, - final ChargeRepositoryWrapper chargeRepositoryWrapper) { + final ChargeRepositoryWrapper chargeRepositoryWrapper, final RoutingDataSource dataSource) { this.glJournalEntryRepository = glJournalEntryRepository; + this.dataSource = dataSource; this.accountMappingRepository = accountMappingRepository; this.closureRepository = closureRepository; this.officeRepositoryWrapper = officeRepositoryWrapper; @@ -119,6 +129,7 @@ public class AccountingProcessorHelper { this.accountRepositoryWrapper = accountRepositoryWrapper; this.clientTransactionRepository = clientTransactionRepositoryWrapper; this.chargeRepositoryWrapper = chargeRepositoryWrapper; + this.jdbcTemplate = new JdbcTemplate(this.dataSource); } public LoanDTO populateLoanDtoFromMap(final Map accountingBridgeData, final boolean cashBasedAccountingEnabled, @@ -842,7 +853,8 @@ public class AccountingProcessorHelper { } private void createCreditJournalEntryForSavings(final Office office, final String currencyCode, final GLAccount account, - final Long savingsId, final String transactionId, final Date transactionDate, final BigDecimal amount) { + final Long savingsId, final String transactionId, final Date transactionDate, final BigDecimal amount) + throws DataAccessException { final boolean manualEntry = false; LoanTransaction loanTransaction = null; SavingsAccountTransaction savingsAccountTransaction = null; @@ -858,6 +870,7 @@ public class AccountingProcessorHelper { final JournalEntry journalEntry = JournalEntry.createNew(office, paymentDetail, account, currencyCode, modifiedTransactionId, manualEntry, transactionDate, JournalEntryType.CREDIT, amount, null, PortfolioProductType.SAVING.getValue(), savingsId, null, loanTransaction, savingsAccountTransaction, clientTransaction, shareTransactionId); + this.glJournalEntryRepository.saveAndFlush(journalEntry); } @@ -948,6 +961,7 @@ public class AccountingProcessorHelper { final JournalEntry journalEntry = JournalEntry.createNew(office, paymentDetail, account, currencyCode, modifiedTransactionId, manualEntry, transactionDate, JournalEntryType.DEBIT, amount, null, PortfolioProductType.SAVING.getValue(), savingsId, null, loanTransaction, savingsAccountTransaction, clientTransaction, shareTransactionId); + this.glJournalEntryRepository.saveAndFlush(journalEntry); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryWritePlatformService.java index 054a134dd..bafdcbdcf 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryWritePlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryWritePlatformService.java @@ -46,4 +46,5 @@ public interface JournalEntryWritePlatformService { void createJournalEntriesForShares(Map accountingBridgeData); void revertShareAccountJournalEntries(ArrayList transactionId, Date transactionDate); + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryWritePlatformServiceJpaRepositoryImpl.java index d91e94b11..3649246b1 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryWritePlatformServiceJpaRepositoryImpl.java @@ -530,7 +530,6 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa .determineProcessor(savingsDTO); accountingProcessorForSavings.createJournalEntriesForSavings(savingsDTO); } - } @Transactional diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java index 51f76b7e8..6b6c5957b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java @@ -31,6 +31,7 @@ import org.apache.fineract.useradministration.domain.Permission; import org.apache.fineract.useradministration.domain.PermissionRepository; import org.apache.fineract.useradministration.exception.PermissionNotFoundException; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -368,6 +369,7 @@ public class ConfigurationDomainServiceJpa implements ConfigurationDomainService return property.getValue(); } + @Cacheable(value = "configByName", key = "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil).getTenant().getTenantIdentifier().concat(#propertyName)") private GlobalConfigurationPropertyData getGlobalConfigurationPropertyData(final String propertyName) { String identifier = ThreadLocalContextUtil.getTenant().getTenantIdentifier(); String key = identifier + "_" + propertyName; diff --git a/fineract-provider/src/main/java/org/apache/fineract/organisation/monetary/data/CurrencyData.java b/fineract-provider/src/main/java/org/apache/fineract/organisation/monetary/data/CurrencyData.java index 0697f2ba3..d4e1f824f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/organisation/monetary/data/CurrencyData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/organisation/monetary/data/CurrencyData.java @@ -61,6 +61,16 @@ public class CurrencyData implements Serializable { this.displayLabel = generateDisplayLabel(); } + public CurrencyData(final String code, final int decimalPlaces, final Integer inMultiplesOf) { + this.code = code; + this.name = null; + this.decimalPlaces = decimalPlaces; + this.inMultiplesOf = inMultiplesOf; + this.displaySymbol = null; + this.nameCode = null; + this.displayLabel = null; + } + public String code() { return this.code; } @@ -108,4 +118,16 @@ public class CurrencyData implements Serializable { public int hashCode() { return Objects.hash(code, name, decimalPlaces, inMultiplesOf, displaySymbol, nameCode, displayLabel); } + + public String getCode() { + return this.code; + } + + public int getDecimalPlaces() { + return this.decimalPlaces; + } + + public Integer getInMultiplesOf() { + return this.inMultiplesOf; + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/organisation/monetary/domain/MonetaryCurrency.java b/fineract-provider/src/main/java/org/apache/fineract/organisation/monetary/domain/MonetaryCurrency.java index 74f84a941..d704b1ed7 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/organisation/monetary/domain/MonetaryCurrency.java +++ b/fineract-provider/src/main/java/org/apache/fineract/organisation/monetary/domain/MonetaryCurrency.java @@ -20,6 +20,7 @@ package org.apache.fineract.organisation.monetary.domain; import javax.persistence.Column; import javax.persistence.Embeddable; +import org.apache.fineract.organisation.monetary.data.CurrencyData; @Embeddable public class MonetaryCurrency { @@ -54,6 +55,10 @@ public class MonetaryCurrency { applicationCurrency.getCurrencyInMultiplesOf()); } + public static MonetaryCurrency fromCurrencyData(final CurrencyData currencyData) { + return new MonetaryCurrency(currencyData.getCode(), currencyData.getDecimalPlaces(), currencyData.getInMultiplesOf()); + } + public String getCode() { return this.code; } @@ -65,4 +70,5 @@ public class MonetaryCurrency { public Integer getCurrencyInMultiplesOf() { return this.inMultiplesOf; } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/organisation/monetary/domain/Money.java b/fineract-provider/src/main/java/org/apache/fineract/organisation/monetary/domain/Money.java index eafa09fdf..652439e51 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/organisation/monetary/domain/Money.java +++ b/fineract-provider/src/main/java/org/apache/fineract/organisation/monetary/domain/Money.java @@ -23,6 +23,7 @@ import java.math.RoundingMode; import java.util.Iterator; import javax.persistence.Column; import javax.persistence.Embeddable; +import org.apache.fineract.organisation.monetary.data.CurrencyData; @Embeddable public class Money implements Comparable { @@ -67,10 +68,18 @@ public class Money implements Comparable { currency.getCurrencyInMultiplesOf()); } + public static Money of(final CurrencyData currency, final BigDecimal newAmount) { + return new Money(currency.getCode(), currency.getDecimalPlaces(), defaultToZeroIfNull(newAmount), currency.getInMultiplesOf()); + } + public static Money zero(final MonetaryCurrency currency) { return new Money(currency.getCode(), currency.getDigitsAfterDecimal(), BigDecimal.ZERO, currency.getCurrencyInMultiplesOf()); } + public static Money zero(final CurrencyData currency) { + return new Money(currency.getCode(), currency.getDecimalPlaces(), BigDecimal.ZERO, currency.getInMultiplesOf()); + } + protected Money() { this.currencyCode = null; this.currencyDigitsAfterDecimal = 0; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/data/ClientData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/data/ClientData.java index deb3cd5ef..1f55f94b3 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/data/ClientData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/data/ClientData.java @@ -129,6 +129,71 @@ public final class ClientData implements Comparable, Serializable { locale, dateFormat); } + public static ClientData createClientForInterestPosting(final Long id, final Long officeId) { + return new ClientData(id, officeId); + } + + private ClientData(final Long clientId, final Long officeId) { + this.rowIndex = null; + this.dateFormat = null; + this.locale = null; + this.firstname = null; + this.lastname = null; + this.middlename = null; + this.fullname = null; + this.activationDate = null; + this.submittedOnDate = null; + this.active = null; + this.externalId = null; + this.officeId = officeId; + this.staffId = null; + this.legalFormId = null; + this.mobileNo = null; + this.dateOfBirth = null; + this.clientTypeId = null; + this.genderId = null; + this.clientClassificationId = null; + this.isStaff = false; + this.address = null; + this.accountNo = null; + this.status = null; + this.subStatus = null; + this.displayName = null; + this.gender = null; + this.clientType = null; + this.clientClassification = null; + this.officeName = null; + this.transferToOfficeId = null; + this.transferToOfficeName = null; + this.imageId = null; + this.imagePresent = null; + this.staffName = null; + this.timeline = null; + this.savingsProductId = null; + this.savingsProductName = null; + this.savingsAccountId = null; + this.legalForm = null; + this.groups = null; + this.officeOptions = null; + this.staffOptions = null; + this.narrations = null; + this.savingProductOptions = null; + this.savingAccountOptions = null; + this.genderOptions = null; + this.clientTypeOptions = null; + this.clientClassificationOptions = null; + this.clientNonPersonConstitutionOptions = null; + this.clientNonPersonMainBusinessLineOptions = null; + this.clientLegalFormOptions = null; + this.clientNonPersonDetails = null; + this.isAddressEnabled = null; + this.datatables = null; + this.familyMemberOptions = null; + this.emailAddress = null; + this.clientCollateralManagements = null; + this.id = clientId; + } + public static ClientData importClientPersonInstance(Long legalFormId, Integer rowIndex, String firstname, String lastname, String middlename, LocalDate submittedOn, LocalDate activationDate, Boolean active, String externalId, Long officeId, Long staffId, String mobileNo, LocalDate dob, Long clientTypeId, Long genderId, Long clientClassificationId, Boolean isStaff, @@ -638,4 +703,5 @@ public final class ClientData implements Comparable, Serializable { public Boolean getIsAddressEnabled() { return this.isAddressEnabled; } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupGeneralData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupGeneralData.java index 17cca883d..65bed689c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupGeneralData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/data/GroupGeneralData.java @@ -126,6 +126,37 @@ public class GroupGeneralData implements Serializable { this.timeline = null; } + public GroupGeneralData(Long id, Long officeId) { + this.id = id; + this.accountNo = null; + this.name = null; + this.externalId = null; + this.status = null; + this.active = null; + this.activationDate = null; + this.officeId = officeId; + this.officeName = null; + this.centerId = null; + this.centerName = null; + this.staffId = null; + this.staffName = null; + this.hierarchy = null; + this.groupLevel = null; + this.clientMembers = null; + this.activeClientMembers = null; + this.groupRoles = null; + this.calendarsData = null; + this.collectionMeetingCalendar = null; + this.centerOptions = null; + this.officeOptions = null; + this.staffOptions = null; + this.clientOptions = null; + this.availableRoles = null; + this.selectedRole = null; + this.closureReasons = null; + this.timeline = null; + } + public GroupGeneralData(Long id) { this.id = id; this.accountNo = null; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymentdetail/data/PaymentDetailData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymentdetail/data/PaymentDetailData.java index 19667e6bf..b510a9c4b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymentdetail/data/PaymentDetailData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymentdetail/data/PaymentDetailData.java @@ -72,4 +72,8 @@ public class PaymentDetailData implements Serializable { public int hashCode() { return Objects.hash(id, paymentType, accountNumber, checkNumber, routingCode, receiptNumber, bankNumber); } + + public PaymentTypeData getPaymentType() { + return this.paymentType; + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountChargeData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountChargeData.java index 715f3f462..8eb09e9c3 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountChargeData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountChargeData.java @@ -86,6 +86,8 @@ public class SavingsAccountChargeData implements Serializable { private final Collection chargeOptions; + private ChargeData chargeData; + public SavingsAccountChargeData(Long chargeId, BigDecimal amount, LocalDate dueDate) { this.chargeId = chargeId; this.amount = amount; @@ -115,6 +117,35 @@ public class SavingsAccountChargeData implements Serializable { this.restartFrequencyEnum = null; } + public SavingsAccountChargeData(Long chargeId, BigDecimal amount, EnumOptionData chargeTimeType, boolean isPenalty) { + this.chargeId = chargeId; + this.amount = amount; + this.dueDate = null; + this.id = null; + this.accountId = null; + this.name = null; + this.chargeTimeType = chargeTimeType; + this.feeOnMonthDay = null; + this.feeInterval = null; + this.chargeCalculationType = null; + this.percentage = null; + this.amountPercentageAppliedTo = null; + this.currency = null; + this.amountPaid = null; + this.amountWaived = null; + this.amountWrittenOff = null; + this.amountOutstanding = null; + this.amountOrPercentage = null; + this.penalty = isPenalty; + this.isActive = null; + this.inactivationDate = null; + this.chargeOptions = null; + this.isFreeWithdrawal = null; + this.freeWithdrawalChargeFrequency = null; + this.restartFrequency = null; + this.restartFrequencyEnum = null; + } + public static SavingsAccountChargeData template(final Collection chargeOptions) { final Long id = null; final Long chargeId = null; @@ -163,6 +194,10 @@ public class SavingsAccountChargeData implements Serializable { restartFrequencyEnum, inactivationDate); } + public boolean isFeeCharge() { + return !this.penalty; + } + private SavingsAccountChargeData(final Long id, final Long chargeId, final Long accountId, final String name, final EnumOptionData chargeTimeType, final LocalDate dueAsOfDate, final EnumOptionData chargeCalculationType, final BigDecimal percentage, final BigDecimal amountPercentageAppliedTo, final CurrencyData currency, final BigDecimal amount, @@ -223,4 +258,20 @@ public class SavingsAccountChargeData implements Serializable { return this.accountId; } + public boolean isPenaltyCharge() { + return this.penalty; + } + + public ChargeData getCharge() { + return this.chargeData; + } + + public Long getId() { + return this.id; + } + + public boolean isPenalty() { + return this.penalty; + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountData.java index 6bba6e7cb..e1722e9a4 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountData.java @@ -21,18 +21,28 @@ package org.apache.fineract.portfolio.savings.data; import java.io.Serializable; import java.math.BigDecimal; import java.time.LocalDate; +import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; +import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Locale; +import java.util.Set; +import java.util.stream.Collectors; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.fineract.infrastructure.core.data.EnumOptionData; +import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.dataqueries.data.DatatableData; import org.apache.fineract.organisation.monetary.data.CurrencyData; import org.apache.fineract.organisation.staff.data.StaffData; import org.apache.fineract.portfolio.charge.data.ChargeData; +import org.apache.fineract.portfolio.client.data.ClientData; +import org.apache.fineract.portfolio.group.data.GroupGeneralData; +import org.apache.fineract.portfolio.savings.DepositAccountType; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransactionSummaryWrapper; +import org.apache.fineract.portfolio.savings.domain.SavingsHelper; import org.apache.fineract.portfolio.tax.data.TaxGroupData; /** @@ -79,7 +89,6 @@ public final class SavingsAccountData implements Serializable { private final Integer daysToDormancy; private final Integer daysToEscheat; private final BigDecimal savingsAmountOnHold; - // associations private final SavingsAccountSummaryData summary; @SuppressWarnings("unused") @@ -104,6 +113,7 @@ public final class SavingsAccountData implements Serializable { private final SavingsAccountChargeData annualFee; private final BigDecimal nominalAnnualInterestRateOverdraft; private final BigDecimal minOverdraftForInterestCalculation; + private transient List savingsAccountTransactionData = new ArrayList<>(); private List datatables = null; @@ -112,7 +122,23 @@ public final class SavingsAccountData implements Serializable { private String locale; private String dateFormat; private transient Integer rowIndex; + private transient Date startInterestCalculationDate; private LocalDate submittedOnDate; + private transient SavingsAccountTransactionSummaryWrapper savingsAccountTransactionSummaryWrapper; + private transient SavingsHelper savingsHelper; + + private transient SavingsAccountSummaryData savingsAccountSummaryData; + private transient Date activatedOnDate; + private transient LocalDate lockedInUntilDate; + private transient ClientData clientData; + private transient SavingsProductData savingsProductData; + private transient List newSavingsAccountTransactionData = new ArrayList<>(); + private transient GroupGeneralData groupGeneralData; + private transient Long officeId; + private transient Set existingTransactionIds = new HashSet<>(); + private transient Set existingReversedTransactionIds = new HashSet<>(); + private transient Long glAccountIdForSavingsControl; + private transient Long glAccountIdForInterestOnSavings; public static SavingsAccountData importInstanceIndividual(Long clientId, Long productId, Long fieldOfficerId, LocalDate submittedOnDate, BigDecimal nominalAnnualInterestRate, EnumOptionData interestCompoundingPeriodTypeEnum, @@ -207,6 +233,63 @@ public final class SavingsAccountData implements Serializable { } }; + public void setNewSavingsAccountTransactionData(final SavingsAccountTransactionData savingsAccountTransactionData) { + this.newSavingsAccountTransactionData.add(savingsAccountTransactionData); + } + + public List getNewSavingsAccountTransactionData() { + return this.newSavingsAccountTransactionData; + } + + public void setSavingsAccountSummaryData(final SavingsAccountSummaryData savingsAccountSummaryData) { + this.savingsAccountSummaryData = savingsAccountSummaryData; + } + + public void setSavingsProduct(final SavingsProductData savingsProductData) { + this.savingsProductData = savingsProductData; + } + + public void setSavingsAccountTransactionData(final SavingsAccountTransactionData savingsAccountTransactionData) { + this.savingsAccountTransactionData.add(savingsAccountTransactionData); + } + + public void setSubmittedOnDate(final LocalDate submittedOnDate) { + this.submittedOnDate = submittedOnDate; + } + + public void setLockedInUntilDate(final LocalDate lockedInUntilDate) { + this.lockedInUntilDate = lockedInUntilDate; + } + + public void setStartInterestCalculationDate(final Date startInterestCalculationDate) { + this.startInterestCalculationDate = startInterestCalculationDate; + } + + public CurrencyData getCurrency() { + return this.currency; + } + + public void setClientData(final ClientData clientData) { + this.clientData = clientData; + } + + public void setGroupGeneralData(final GroupGeneralData groupGeneralData) { + this.groupGeneralData = groupGeneralData; + } + + public void setUpdatedTransactions(List savingsAccountTransactionDataList) { + this.savingsAccountTransactionData = new ArrayList<>(); + this.savingsAccountTransactionData.addAll(savingsAccountTransactionDataList); + } + + public void setOfficeId(final Long officeId) { + this.officeId = officeId; + } + + public Long getOfficeId() { + return this.officeId; + } + public String getClientName() { return clientName; } @@ -215,6 +298,22 @@ public final class SavingsAccountData implements Serializable { return accountNo; } + public BigDecimal getNominalAnnualInterestRate() { + return this.nominalAnnualInterestRate; + } + + public BigDecimal getNominalAnnualInterestRateOverdraft() { + return this.nominalAnnualInterestRateOverdraft; + } + + public boolean isAllowOverdraft() { + return this.allowOverdraft; + } + + public TaxGroupData getTaxGroupData() { + return this.taxGroup; + } + public Long getClientId() { return clientId; } @@ -231,6 +330,169 @@ public final class SavingsAccountData implements Serializable { return timeline; } + public Long getId() { + return this.id; + } + + public void updateTransactions(final SavingsAccountTransactionData savingsAccountTransactionData) { + this.savingsAccountTransactionData.add(savingsAccountTransactionData); + } + + public boolean withHoldTax() { + return this.withHoldTax; + } + + public DepositAccountType depositAccountType() { + return DepositAccountType.fromInt(this.depositType.getId().intValue()); + } + + public void setGlAccountIdForSavingsControl(final Long glAccountIdForSavingsControl) { + this.glAccountIdForSavingsControl = glAccountIdForSavingsControl; + } + + public void setGlAccountIdForInterestOnSavings(final Long glAccountIdForInterestOnSavings) { + this.glAccountIdForInterestOnSavings = glAccountIdForInterestOnSavings; + } + + public Long getGlAccountIdForSavingsControl() { + return this.glAccountIdForSavingsControl; + } + + public Long getGlAccountIdForInterestOnSavings() { + return this.glAccountIdForInterestOnSavings; + } + + public SavingsAccountSummaryData getSavingsAccountSummaryData() { + return this.savingsAccountSummaryData; + } + + public List getSavingsAccountTransactionData() { + return this.savingsAccountTransactionData; + } + + public void setHelpers(final SavingsAccountTransactionSummaryWrapper savingsAccountTransactionSummaryWrapper, + final SavingsHelper savingsHelper) { + this.savingsAccountTransactionSummaryWrapper = savingsAccountTransactionSummaryWrapper; + this.savingsHelper = savingsHelper; + } + + public BigDecimal getMinBalanceForInterestCalculation() { + return this.minBalanceForInterestCalculation; + } + + public BigDecimal getMinOverdraftForInterestCalculation() { + return this.minOverdraftForInterestCalculation; + } + + public Integer getInterestPostingPeriodType() { + return this.interestPostingPeriodType.getId().intValue(); + } + + public Integer getDepositType() { + return this.depositType.getId().intValue(); + } + + public Integer getInterestCompoundingPeriodType() { + return this.interestCompoundingPeriodType.getId().intValue(); + } + + public Integer getInterestCalculationType() { + return this.interestCalculationType.getId().intValue(); + } + + public Integer getInterestCalculationDaysInYearType() { + return this.interestCalculationDaysInYearType.getId().intValue(); + } + + public LocalDate getLockedInUntilDate() { + return this.lockedInUntilDate; + } + + public SavingsAccountTransactionData findLastTransaction(final LocalDate date) { + + SavingsAccountTransactionData savingsTransaction = null; + List trans = getTransactions(); + for (final SavingsAccountTransactionData transaction : trans) { + if (transaction.isNotReversed() && transaction.occursOn(date)) { + savingsTransaction = transaction; + break; + } + } + + return savingsTransaction; + } + + public List getTransactions() { + return this.savingsAccountTransactionData; + } + + public LocalDate getStartInterestCalculationDate() { + LocalDate startInterestCalculationLocalDate = null; + if (this.startInterestCalculationDate != null) { + startInterestCalculationLocalDate = LocalDate.ofInstant(this.startInterestCalculationDate.toInstant(), + DateUtils.getDateTimeZoneOfTenant()); + } else { + startInterestCalculationLocalDate = getActivationLocalDate(); + } + return startInterestCalculationLocalDate; + } + + public LocalDate getActivationLocalDate() { + LocalDate activationLocalDate = null; + if (this.timeline.getActivatedOnDate() != null) { + activationLocalDate = this.timeline.getActivatedOnDate(); + } + return activationLocalDate; + } + + public Integer getLockinPeriodFrequencyType() { + return this.lockinPeriodFrequencyType.getId().intValue(); + } + + public Collection findCurrentTransactionIdsWithPivotDateConfig() { + + final Collection ids = new ArrayList<>(); + List trans = this.savingsAccountTransactionData; + for (final SavingsAccountTransactionData transaction : trans) { + ids.add(transaction.getId()); + } + return ids; + } + + public Collection findCurrentReversedTransactionIdsWithPivotDateConfig() { + final Collection ids = new ArrayList<>(); + List trans = this.savingsAccountTransactionData; + // time consuming + for (final SavingsAccountTransactionData transaction : trans) { + if (transaction.isReversed()) { + ids.add(transaction.getId()); + } + } + return ids; + } + + public Long officeId() { + Long officeId = null; + if (this.clientData != null) { + officeId = this.clientData.officeId(); + } else if (this.groupId != null) { + officeId = this.groupGeneralData.officeId(); + } + return officeId; + } + + public List getSavingsAccountTransactionsWithPivotConfig() { + return this.transactions.stream().collect(Collectors.toList()); + } + + public Boolean isAccrualBasedAccountingEnabledOnSavingsProduct() { + return this.savingsProductData.isAccrualBasedAccountingEnabled(); + } + + public Boolean isCashBasedAccountingEnabledOnSavingsProduct() { + return this.savingsProductData.isCashBasedAccountingEnabled(); + } + public static SavingsAccountData importInstanceGroup(Long groupId, Long productId, Long fieldOfficerId, LocalDate submittedOnDate, BigDecimal nominalAnnualInterestRate, EnumOptionData interestCompoundingPeriodTypeEnum, EnumOptionData interestPostingPeriodTypeEnum, EnumOptionData interestCalculationTypeEnum, @@ -765,6 +1027,26 @@ public final class SavingsAccountData implements Serializable { return this.currency; } + public SavingsAccountTransactionSummaryWrapper getSavingsAccountTransactionSummaryWrapper() { + return this.savingsAccountTransactionSummaryWrapper; + } + + public void setExistingTransactionIds(final Set existingTransactionIds) { + if (existingTransactionIds != null) { + this.existingTransactionIds.addAll(existingTransactionIds); + } + } + + public void setExistingReversedTransactionIds(final Set existingReversedTransactionIds) { + if (existingReversedTransactionIds != null) { + this.existingReversedTransactionIds.addAll(existingReversedTransactionIds); + } + } + + public SavingsHelper getSavingsHelper() { + return this.savingsHelper; + } + @Override public boolean equals(final Object obj) { @@ -793,4 +1075,16 @@ public final class SavingsAccountData implements Serializable { public void setDatatables(final List datatables) { this.datatables = datatables; } + + public SavingsAccountSummaryData getSummary() { + return this.summary; + } + + public Set getExistingTransactionIds() { + return this.existingTransactionIds; + } + + public Set getExistingReversedTransactionIds() { + return this.existingReversedTransactionIds; + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountSummaryData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountSummaryData.java index db2bd26a7..fbbe5cccb 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountSummaryData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountSummaryData.java @@ -21,7 +21,16 @@ package org.apache.fineract.portfolio.savings.data; import java.io.Serializable; import java.math.BigDecimal; import java.time.LocalDate; +import java.util.HashMap; +import java.util.List; +import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.organisation.monetary.data.CurrencyData; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransactionSummaryWrapper; +import org.apache.fineract.portfolio.savings.domain.interest.PostingPeriod; /** * Immutable data object representing Savings Account summary information. @@ -30,21 +39,23 @@ import org.apache.fineract.organisation.monetary.data.CurrencyData; public class SavingsAccountSummaryData implements Serializable { private final CurrencyData currency; - private final BigDecimal totalDeposits; - private final BigDecimal totalWithdrawals; - private final BigDecimal totalWithdrawalFees; - private final BigDecimal totalAnnualFees; - private final BigDecimal totalInterestEarned; - private final BigDecimal totalInterestPosted; - private final BigDecimal accountBalance; - private final BigDecimal totalFeeCharge; - private final BigDecimal totalPenaltyCharge; - private final BigDecimal totalOverdraftInterestDerived; - private final BigDecimal totalWithholdTax; - private final BigDecimal interestNotPosted; - private final LocalDate lastInterestCalculationDate; - private final BigDecimal availableBalance; - private final LocalDate interestPostedTillDate; + private BigDecimal totalDeposits; + private BigDecimal totalWithdrawals; + private BigDecimal totalWithdrawalFees; + private BigDecimal totalAnnualFees; + private BigDecimal totalInterestEarned; + private BigDecimal totalInterestPosted; + private BigDecimal accountBalance; + private BigDecimal totalFeeCharge; + private BigDecimal totalPenaltyCharge; + private BigDecimal totalOverdraftInterestDerived; + private BigDecimal totalWithholdTax; + private BigDecimal interestNotPosted; + private LocalDate lastInterestCalculationDate; + private BigDecimal availableBalance; + private LocalDate interestPostedTillDate; + private LocalDate prevInterestPostedTillDate; + private transient BigDecimal runningBalanceOnInterestPostingTillDate = BigDecimal.ZERO; public SavingsAccountSummaryData(final CurrencyData currency, final BigDecimal totalDeposits, final BigDecimal totalWithdrawals, final BigDecimal totalWithdrawalFees, final BigDecimal totalAnnualFees, final BigDecimal totalInterestEarned, @@ -69,4 +80,228 @@ public class SavingsAccountSummaryData implements Serializable { this.availableBalance = availableBalance; this.interestPostedTillDate = interestPostedTillDate; } + + public void setPrevInterestPostedTillDate(LocalDate interestPostedTillDate) { + this.prevInterestPostedTillDate = interestPostedTillDate; + } + + public LocalDate getPrevInterestPostedTillDate() { + return this.prevInterestPostedTillDate; + } + + public LocalDate getInterestPostedTillDate() { + return this.interestPostedTillDate; + } + + public BigDecimal getTotalInterestPosted() { + return this.totalInterestPosted; + } + + public BigDecimal getTotalWithdrawalFees() { + return this.totalWithdrawalFees; + } + + public BigDecimal getTotalInterestEarned() { + return this.totalInterestEarned; + } + + public BigDecimal getTotalDeposits() { + return this.totalDeposits; + } + + public BigDecimal getTotalWithdrawals() { + return this.totalWithdrawals; + } + + public BigDecimal getTotalFeeCharge() { + return this.totalFeeCharge; + } + + public BigDecimal getAvailableBalance() { + return this.availableBalance; + } + + public BigDecimal getTotalOverdraftInterestDerived() { + return this.totalOverdraftInterestDerived; + } + + public BigDecimal getTotalWithholdTax() { + return this.totalWithholdTax; + } + + public BigDecimal getTotalPenaltyCharge() { + return this.totalPenaltyCharge; + } + + public BigDecimal getTotalAnnualFees() { + return this.totalAnnualFees; + } + + public LocalDate getLastInterestCalculationDate() { + return this.lastInterestCalculationDate; + } + + public BigDecimal getRunningBalanceOnPivotDate() { + return this.runningBalanceOnInterestPostingTillDate; + } + + public void updateSummaryWithPivotConfig(final CurrencyData currency, final SavingsAccountTransactionSummaryWrapper wrapper, + final SavingsAccountTransaction transaction, final List savingsAccountTransactions) { + + if (transaction != null) { + Money transactionAmount = Money.of(currency, transaction.getAmount()); + switch (SavingsAccountTransactionType.fromInt(transaction.getTypeOf())) { + case DEPOSIT: + if (transaction.isDepositAndNotReversed() || transaction.isDividendPayoutAndNotReversed()) { + this.totalDeposits = Money.of(currency, this.totalDeposits).plus(transactionAmount).getAmount(); + this.accountBalance = Money.of(currency, this.accountBalance).plus(transactionAmount).getAmount(); + } + break; + case WITHDRAWAL: + if (transaction.isWithdrawal() && transaction.isNotReversed()) { + this.totalWithdrawals = Money.of(currency, this.totalWithdrawals).plus(transactionAmount).getAmount(); + this.accountBalance = Money.of(currency, this.accountBalance).minus(transactionAmount).getAmount(); + } + break; + case WITHDRAWAL_FEE: + if (transaction.isWithdrawalFeeAndNotReversed() && transaction.isNotReversed()) { + this.totalWithdrawalFees = Money.of(currency, this.totalWithdrawalFees).plus(transactionAmount).getAmount(); + this.totalFeeCharge = Money.of(currency, this.totalFeeCharge).plus(transactionAmount).getAmount(); + this.accountBalance = Money.of(currency, this.accountBalance).minus(transactionAmount).getAmount(); + } + break; + case ANNUAL_FEE: + if (transaction.isAnnualFeeAndNotReversed() && transaction.isNotReversed()) { + this.totalAnnualFees = Money.of(currency, this.totalAnnualFees).plus(transactionAmount).getAmount(); + this.totalFeeCharge = Money.of(currency, this.totalFeeCharge).plus(transactionAmount).getAmount(); + this.accountBalance = Money.of(currency, this.accountBalance).minus(transactionAmount).getAmount(); + } + break; + case WAIVE_CHARGES: + if (transaction.isWaiveFeeChargeAndNotReversed()) { + this.totalFeeCharge = Money.of(currency, this.totalFeeCharge).plus(transactionAmount.getAmount()).getAmount(); + } else if (transaction.isWaivePenaltyChargeAndNotReversed()) { + this.totalPenaltyCharge = Money.of(currency, this.totalPenaltyCharge).plus(transactionAmount.getAmount()) + .getAmount(); + } + break; + case PAY_CHARGE: + if (transaction.isFeeChargeAndNotReversed()) { + this.totalFeeCharge = Money.of(currency, this.totalFeeCharge).plus(transactionAmount).getAmount(); + } else if (transaction.isPenaltyChargeAndNotReversed()) { + this.totalPenaltyCharge = Money.of(currency, this.totalPenaltyCharge).plus(transactionAmount).getAmount(); + } + if (transaction.isFeeChargeAndNotReversed() || transaction.isPenaltyChargeAndNotReversed()) { + this.accountBalance = Money.of(currency, this.accountBalance).minus(transactionAmount).getAmount(); + } + break; + case OVERDRAFT_INTEREST: + if (transaction.isOverdraftInterestAndNotReversed()) { + this.totalOverdraftInterestDerived = Money.of(currency, this.totalOverdraftInterestDerived).plus(transactionAmount) + .getAmount(); + this.accountBalance = Money.of(currency, this.accountBalance).minus(transactionAmount).getAmount(); + } + break; + case WITHHOLD_TAX: + if (transaction.isWithHoldTaxAndNotReversed()) { + this.totalWithholdTax = Money.of(currency, this.totalWithholdTax).plus(transactionAmount).getAmount(); + this.accountBalance = Money.of(currency, this.accountBalance).minus(transactionAmount).getAmount(); + } + break; + default: + break; + } + } else { + // boolean isUpdated = false; + Money interestTotal = Money.of(currency, this.totalInterestPosted); + Money withHoldTaxTotal = Money.of(currency, this.totalWithholdTax); + + final HashMap map = updateRunningBalanceAndPivotDate(true, savingsAccountTransactions, interestTotal, + withHoldTaxTotal, currency); + interestTotal = map.get("interestTotal"); + withHoldTaxTotal = map.get("withHoldTax"); + this.totalInterestPosted = interestTotal.getAmountDefaultedToNullIfZero(); + this.totalWithholdTax = withHoldTaxTotal.getAmountDefaultedToNullIfZero(); + this.accountBalance = Money.of(currency, this.accountBalance).plus(this.totalInterestPosted).minus(this.totalWithholdTax) + .getAmount(); + } + } + + public void updateFromInterestPeriodSummaries(final MonetaryCurrency currency, final List allPostingPeriods) { + + Money totalEarned = Money.zero(currency); + LocalDate interestCalculationDate = DateUtils.getLocalDateOfTenant(); + for (final PostingPeriod period : allPostingPeriods) { + Money interestEarned = period.interest(); + interestEarned = interestEarned == null ? Money.zero(currency) : interestEarned; + totalEarned = totalEarned.plus(interestEarned); + } + this.lastInterestCalculationDate = interestCalculationDate; + this.totalInterestEarned = totalEarned.getAmount(); + } + + @SuppressWarnings("unchecked") + private HashMap updateRunningBalanceAndPivotDate(final boolean backdatedTxnsAllowedTill, + final List savingsAccountTransactions, Money interestTotal, Money withHoldTaxTotal, + CurrencyData currency) { + boolean isUpdated = false; + HashMap map = new HashMap<>(); + for (int i = savingsAccountTransactions.size() - 1; i >= 0; i--) { + final SavingsAccountTransactionData savingsAccountTransaction = savingsAccountTransactions.get(i); + if (savingsAccountTransaction.isInterestPostingAndNotReversed() && savingsAccountTransaction.isNotReversed() && !isUpdated) { + setRunningBalanceOnPivotDate(savingsAccountTransaction.getRunningBalance(currency).getAmount()); + setInterestPostedTillDate(savingsAccountTransaction.getTransactionDate()); + isUpdated = true; + if (!backdatedTxnsAllowedTill) { + break; + } + } + if (backdatedTxnsAllowedTill) { + if (savingsAccountTransaction.isInterestPostingAndNotReversed() && savingsAccountTransaction.isNotReversed()) { + interestTotal = interestTotal.plus(savingsAccountTransaction.getAmount()); + } + + if (savingsAccountTransaction.isWithHoldTaxAndNotReversed()) { + withHoldTaxTotal = withHoldTaxTotal.plus(savingsAccountTransaction.getAmount()); + } + } + } + if (backdatedTxnsAllowedTill) { + map.put("interestTotal", interestTotal); + map.put("withHoldTax", withHoldTaxTotal); + } + return map; + } + + public void updateSummary(final CurrencyData currency, final SavingsAccountTransactionSummaryWrapper wrapper, + final List transactions) { + + this.totalDeposits = wrapper.calculateTotalDeposits(currency, transactions); + this.totalWithdrawals = wrapper.calculateTotalWithdrawals(currency, transactions); + this.totalInterestPosted = wrapper.calculateTotalInterestPosted(currency, transactions); + this.totalWithdrawalFees = wrapper.calculateTotalWithdrawalFees(currency, transactions); + this.totalAnnualFees = wrapper.calculateTotalAnnualFees(currency, transactions); + this.totalFeeCharge = wrapper.calculateTotalFeesCharge(currency, transactions); + this.totalPenaltyCharge = wrapper.calculateTotalPenaltyCharge(currency, transactions); + this.totalFeeCharge = wrapper.calculateTotalFeesChargeWaived(currency, transactions); + this.totalPenaltyCharge = wrapper.calculateTotalPenaltyChargeWaived(currency, transactions); + this.totalOverdraftInterestDerived = wrapper.calculateTotalOverdraftInterest(currency, transactions); + this.totalWithholdTax = wrapper.calculateTotalWithholdTaxWithdrawal(currency, transactions); + + // boolean isUpdated = false; + updateRunningBalanceAndPivotDate(false, transactions, null, null, currency); + + this.accountBalance = Money.of(currency, this.totalDeposits).plus(this.totalInterestPosted).minus(this.totalWithdrawals) + .minus(this.totalWithdrawalFees).minus(this.totalAnnualFees).minus(this.totalFeeCharge).minus(this.totalPenaltyCharge) + .minus(totalOverdraftInterestDerived).minus(totalWithholdTax).getAmount(); + } + + public void setRunningBalanceOnPivotDate(final BigDecimal runningBalanceOnPivotDate) { + this.runningBalanceOnInterestPostingTillDate = runningBalanceOnPivotDate; + } + + public void setInterestPostedTillDate(final LocalDate date) { + this.interestPostedTillDate = date; + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionData.java index 6fb9cc0b1..3ff8c0692 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionData.java @@ -21,15 +21,32 @@ package org.apache.fineract.portfolio.savings.data; import java.io.Serializable; import java.math.BigDecimal; import java.time.LocalDate; +import java.time.ZoneId; +import java.util.ArrayList; import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; import org.apache.fineract.infrastructure.codes.data.CodeValueData; +import org.apache.fineract.infrastructure.core.domain.LocalDateInterval; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.organisation.monetary.data.CurrencyData; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.portfolio.account.data.AccountTransferData; import org.apache.fineract.portfolio.paymentdetail.data.PaymentDetailData; import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData; import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountChargesPaidByData; +import org.apache.fineract.portfolio.savings.domain.interest.EndOfDayBalance; import org.apache.fineract.portfolio.savings.service.SavingsEnumerations; +import org.apache.fineract.portfolio.tax.data.TaxComponentData; +import org.apache.fineract.portfolio.tax.data.TaxDetailsData; +import org.springframework.util.CollectionUtils; /** * Immutable data object representing a savings account transaction. @@ -37,7 +54,7 @@ import org.apache.fineract.portfolio.savings.service.SavingsEnumerations; @SuppressWarnings("unused") public final class SavingsAccountTransactionData implements Serializable { - private final Long id; + private Long id; private final SavingsAccountTransactionEnumData transactionType; private final Long accountId; private final String accountNo; @@ -46,13 +63,15 @@ public final class SavingsAccountTransactionData implements Serializable { private final PaymentDetailData paymentDetailData; private final BigDecimal amount; private final BigDecimal outstandingChargeAmount; - private final BigDecimal runningBalance; - private final boolean reversed; + private BigDecimal runningBalance; + private boolean reversed; private final AccountTransferData transfer; - private final LocalDate submittedOnDate; + private Date submittedOnDate; private final boolean interestedPostedAsOn; private final String submittedByUsername; private final String note; + private final boolean isManualTransaction; + private Set chargesPaidByData = new HashSet<>(); // templates final Collection paymentTypeOptions; @@ -70,6 +89,13 @@ public final class SavingsAccountTransactionData implements Serializable { private String routingCode; private String receiptNumber; private String bankNumber; + private BigDecimal cumulativeBalance; + private LocalDate balanceEndDate; + private transient List taxDetails = new ArrayList<>(); + private Integer balanceNumberOfDays; + private BigDecimal overdraftAmount; + private transient Long modifiedId; + private transient String refNo; public static SavingsAccountTransactionData importInstance(BigDecimal transactionAmount, LocalDate transactionDate, Long paymentTypeId, String accountNumber, String checkNumber, String routingCode, String receiptNumber, String bankNumber, Long savingsAccountId, @@ -78,6 +104,442 @@ public final class SavingsAccountTransactionData implements Serializable { receiptNumber, bankNumber, savingsAccountId, transactionType, rowIndex, locale, dateFormat); } + public static SavingsAccountTransactionData interestPosting(final SavingsAccountData savingsAccount, final LocalDate date, + final Money amount, final boolean isManualTransaction) { + final boolean isReversed = false; + final SavingsAccountTransactionType savingsAccountTransactionType = SavingsAccountTransactionType.INTEREST_POSTING; + SavingsAccountTransactionEnumData savingsAccountTransactionEnumData = new SavingsAccountTransactionEnumData( + savingsAccountTransactionType.getValue().longValue(), savingsAccountTransactionType.getCode(), + savingsAccountTransactionType.getValue().toString()); + return new SavingsAccountTransactionData(amount.getAmount(), date, savingsAccount.getId(), savingsAccountTransactionEnumData, + isReversed, null, isManualTransaction); + } + + public static SavingsAccountTransactionData overdraftInterest(final SavingsAccountData savingsAccount, final LocalDate date, + final Money amount, final boolean isManualTransaction) { + final boolean isReversed = false; + final SavingsAccountTransactionType savingsAccountTransactionType = SavingsAccountTransactionType.OVERDRAFT_INTEREST; + SavingsAccountTransactionEnumData savingsAccountTransactionEnumData = new SavingsAccountTransactionEnumData( + savingsAccountTransactionType.getValue().longValue(), savingsAccountTransactionType.getCode(), + savingsAccountTransactionType.getValue().toString()); + return new SavingsAccountTransactionData(amount.getAmount(), date, savingsAccount.getId(), savingsAccountTransactionEnumData, + isReversed, null, isManualTransaction); + } + + public List getTaxDetails() { + return this.taxDetails; + } + + public boolean isInterestPostingAndNotReversed() { + return this.transactionType.isInterestPosting() && isNotReversed(); + } + + public void setTaxDetails(final TaxDetailsData taxDetails) { + this.taxDetails.add(taxDetails); + } + + public boolean isOverdraftInterestAndNotReversed() { + return this.transactionType.isIncomeFromInterest() && isNotReversed(); + } + + public boolean isDebit() { + return isWithdrawal() || isWithdrawalFeeAndNotReversed() || isAnnualFeeAndNotReversed() || isPayCharge() + || isOverdraftInterestAndNotReversed() || isWithHoldTaxAndNotReversed(); + } + + public boolean isWithdrawalFeeAndNotReversed() { + return this.transactionType.isWithdrawalFee() && isNotReversed(); + } + + public boolean isPayCharge() { + return this.transactionType.isPayCharge(); + } + + public void updateRunningBalance(final Money balance) { + this.runningBalance = balance.getAmount(); + } + + public void updateOverdraftAmount(BigDecimal overdraftAmount) { + this.overdraftAmount = overdraftAmount; + } + + public boolean isAmountOnHold() { + return this.transactionType.isAmountOnHold(); + } + + public boolean isAnnualFeeAndNotReversed() { + return isAnnualFee() && isNotReversed(); + } + + public boolean isAnnualFee() { + return this.transactionType.isAnnualFee(); + } + + public Money getRunningBalance(final CurrencyData currency) { + return Money.of(currency, this.runningBalance); + } + + public boolean isDepositAndNotReversed() { + return this.transactionType.isDeposit() && isNotReversed(); + } + + public boolean isDividendPayoutAndNotReversed() { + return this.transactionType.isDividendPayout() && isNotReversed(); + } + + public void setRefNo(final String uuid) { + this.refNo = uuid; + } + + public String getRefNo() { + return this.refNo; + } + + public void setBalanceNumberOfDays(final Integer balanceNumberOfDays) { + this.balanceNumberOfDays = balanceNumberOfDays; + } + + public Integer getBalanceNumberOfDays() { + return this.balanceNumberOfDays; + } + + public EndOfDayBalance toEndOfDayBalance(final Money openingBalance) { + final MonetaryCurrency currency = openingBalance.getCurrency(); + Money endOfDayBalance = openingBalance.copy(); + if (isDeposit() || isDividendPayoutAndNotReversed()) { + endOfDayBalance = openingBalance.plus(getAmount()); + } else if (isWithdrawal() || isChargeTransactionAndNotReversed()) { + + if (openingBalance.isGreaterThanZero()) { + endOfDayBalance = openingBalance.minus(getAmount()); + } else { + endOfDayBalance = Money.of(currency, this.runningBalance); + } + } + + return EndOfDayBalance.from(getTransactionLocalDate(), openingBalance, endOfDayBalance, this.balanceNumberOfDays); + } + + public boolean isChargeTransactionAndNotReversed() { + return this.transactionType.isChargeTransaction() && isNotReversed(); + } + + public LocalDate getLastTransactionDate() { + return this.transactionDate; + } + + public boolean occursOn(final LocalDate occursOnDate) { + return getTransactionLocalDate().isEqual(occursOnDate); + } + + public LocalDate getTransactionLocalDate() { + return this.transactionDate; + } + + public EndOfDayBalance toEndOfDayBalanceBoundedBy(final Money openingBalance, final LocalDateInterval boundedBy) { + + final MonetaryCurrency currency = openingBalance.getCurrency(); + Money endOfDayBalance = openingBalance.copy(); + + int numberOfDaysOfBalance = this.balanceNumberOfDays; + + LocalDate balanceStartDate = getTransactionLocalDate(); + LocalDate balanceEndDate = getEndOfBalanceLocalDate(); + + if (boundedBy.startDate().isAfter(balanceStartDate)) { + balanceStartDate = boundedBy.startDate(); + final LocalDateInterval spanOfBalance = LocalDateInterval.create(balanceStartDate, balanceEndDate); + numberOfDaysOfBalance = spanOfBalance.daysInPeriodInclusiveOfEndDate(); + } else { + if (isDeposit() || isDividendPayoutAndNotReversed()) { + endOfDayBalance = endOfDayBalance.plus(getAmount()); + } else if (isWithdrawal() || isChargeTransactionAndNotReversed()) { + if (endOfDayBalance.isGreaterThanZero()) { + endOfDayBalance = endOfDayBalance.minus(getAmount()); + } else { + endOfDayBalance = Money.of(currency, this.runningBalance); + } + } + } + + if (balanceEndDate.isAfter(boundedBy.endDate())) { + balanceEndDate = boundedBy.endDate(); + final LocalDateInterval spanOfBalance = LocalDateInterval.create(balanceStartDate, balanceEndDate); + numberOfDaysOfBalance = spanOfBalance.daysInPeriodInclusiveOfEndDate(); + } + + return EndOfDayBalance.from(balanceStartDate, openingBalance, endOfDayBalance, numberOfDaysOfBalance); + } + + public void reverse() { + this.reversed = true; + } + + public boolean fallsWithin(final LocalDateInterval periodInterval) { + final LocalDateInterval balanceInterval = LocalDateInterval.create(getTransactionLocalDate(), getEndOfBalanceLocalDate()); + return periodInterval.contains(balanceInterval); + } + + public LocalDate getEndOfBalanceLocalDate() { + return this.balanceEndDate == null ? null : this.balanceEndDate; + } + + public void zeroBalanceFields() { + this.runningBalance = null; + this.cumulativeBalance = null; + this.balanceEndDate = null; + this.balanceNumberOfDays = null; + } + + public boolean isAmountRelease() { + return this.transactionType.isAmountRelease(); + } + + public boolean isCredit() { + return isDeposit() || isInterestPostingAndNotReversed() || isDividendPayoutAndNotReversed(); + } + + public boolean isDeposit() { + return this.transactionType.isDeposit(); + } + + public static SavingsAccountTransactionData copyTransaction(SavingsAccountTransactionData accountTransaction) { + return new SavingsAccountTransactionData(accountTransaction.getSavingsAccountId(), null, accountTransaction.getPaymentDetailData(), + accountTransaction.getTransactionType(), accountTransaction.getTransactionLocalDate(), + accountTransaction.getSubmittedOnDate(), accountTransaction.getAmount(), accountTransaction.isReversed(), null, + accountTransaction.isManualTransaction()); + } + + private SavingsAccountTransactionData(final Long savingsId, final Long officeId, final PaymentDetailData paymentDetailData, + final SavingsAccountTransactionEnumData savingsAccountTransactionType, final LocalDate transactionDate, final Date createdDate, + final BigDecimal amount, final boolean isReversed, final Long userId, final boolean isManualTransaction) { + this.savingsAccountId = savingsId; + this.paymentDetailData = paymentDetailData; + this.transactionType = savingsAccountTransactionType; + this.transactionDate = transactionDate; + this.submittedOnDate = createdDate; + this.amount = amount; + this.isManualTransaction = isManualTransaction; + this.id = null; + this.accountId = null; + this.accountNo = null; + this.date = null; + this.currency = null; + this.outstandingChargeAmount = null; + this.runningBalance = null; + this.reversed = isReversed; + this.transfer = null; + this.interestedPostedAsOn = false; + this.rowIndex = null; + this.dateFormat = null; + this.locale = null; + this.transactionAmount = null; + this.paymentTypeId = null; + this.accountNumber = null; + this.checkNumber = null; + this.routingCode = null; + this.receiptNumber = null; + this.bankNumber = null; + this.paymentTypeOptions = null; + this.submittedByUsername = null; + this.note = null; + } + + public boolean isChargeTransaction() { + return this.transactionType.isChargeTransaction(); + } + + public Set getSavingsAccountChargesPaid() { + return this.chargesPaidByData; + } + + public void updateCumulativeBalanceAndDates(final MonetaryCurrency currency, final LocalDate endOfBalanceDate) { + // balance end date should not be before transaction date + if (endOfBalanceDate != null && endOfBalanceDate.isBefore(this.transactionDate)) { + this.balanceEndDate = this.transactionDate; + } else if (endOfBalanceDate != null) { + this.balanceEndDate = endOfBalanceDate; + } else { + this.balanceEndDate = null; + } + this.balanceNumberOfDays = LocalDateInterval.create(getTransactionLocalDate(), endOfBalanceDate).daysInPeriodInclusiveOfEndDate(); + this.cumulativeBalance = Money.of(currency, this.runningBalance).multipliedBy(this.balanceNumberOfDays).getAmount(); + } + + public boolean hasNotAmount(final Money amountToCheck) { + final Money transactionAmount = getAmount(amountToCheck.getCurrency()); + return transactionAmount.isNotEqualTo(amountToCheck); + } + + public boolean isFeeChargeAndNotReversed() { + return isFeeCharge() && isNotReversed(); + } + + public boolean isFeeCharge() { + final SavingsAccountChargesPaidByData chargePaidBy = getSavingsAccountChargePaidBy(); + return (isPayCharge() && chargePaidBy != null) ? chargePaidBy.isFeeCharge() : false; + } + + public void setChargesPaidByData(final SavingsAccountChargesPaidByData savingsAccountChargesPaidByData) { + this.chargesPaidByData.add(savingsAccountChargesPaidByData); + } + + public void setOverdraftAmount(final BigDecimal overdraftAmount) { + this.overdraftAmount = overdraftAmount; + } + + public boolean isPenaltyChargeAndNotReversed() { + return isPenaltyCharge() && isNotReversed(); + } + + public boolean isPenaltyCharge() { + final SavingsAccountChargesPaidByData chargePaidBy = getSavingsAccountChargePaidBy(); + return (isPayCharge() && chargePaidBy != null) ? chargePaidBy.isPenaltyCharge() : false; + } + + public boolean isWaiveFeeChargeAndNotReversed() { + return isWaiveFeeCharge() && isNotReversed(); + } + + public boolean isWaiveFeeCharge() { + final SavingsAccountChargesPaidByData chargePaidBy = getSavingsAccountChargePaidBy(); + return (isWaiveCharge() && chargePaidBy != null) ? chargePaidBy.isFeeCharge() : false; + } + + public boolean isWaiveCharge() { + return SavingsAccountTransactionType.fromInt(this.transactionType.getId().intValue()).isWaiveCharge(); + } + + public boolean isWaivePenaltyChargeAndNotReversed() { + return isWaivePenaltyCharge() && isNotReversed(); + } + + public boolean isWaivePenaltyCharge() { + final SavingsAccountChargesPaidByData chargePaidBy = getSavingsAccountChargePaidBy(); + return (isWaiveCharge() && chargePaidBy != null) ? chargePaidBy.isPenaltyCharge() : false; + } + + private SavingsAccountChargesPaidByData getSavingsAccountChargePaidBy() { + if (!CollectionUtils.isEmpty(this.chargesPaidByData)) { + return this.chargesPaidByData.iterator().next(); + } + return null; + } + + public Money getAmount(final MonetaryCurrency currency) { + return Money.of(currency, this.amount); + } + + public static SavingsAccountTransactionData withHoldTax(final SavingsAccountData savingsAccount, final LocalDate date, + final Money amount, final Map taxDetails) { + final boolean isReversed = false; + final boolean isManualTransaction = false; + SavingsAccountTransactionType savingsAccountTransactionType = SavingsAccountTransactionType.WITHHOLD_TAX; + SavingsAccountTransactionEnumData transactionType = new SavingsAccountTransactionEnumData( + savingsAccountTransactionType.getValue().longValue(), savingsAccountTransactionType.getCode(), + savingsAccountTransactionType.getValue().toString()); + SavingsAccountTransactionData accountTransaction = new SavingsAccountTransactionData(amount.getAmount(), date, + savingsAccount.getId(), transactionType, isReversed, null, isManualTransaction); + updateTaxDetails(taxDetails, accountTransaction); + return accountTransaction; + } + + public static void updateTaxDetails(final Map taxDetails, + final SavingsAccountTransactionData accountTransaction) { + if (taxDetails != null) { + for (Map.Entry mapEntry : taxDetails.entrySet()) { + accountTransaction.getTaxDetails().add(new TaxDetailsData(mapEntry.getKey(), mapEntry.getValue())); + } + } + } + + public Map toMapData(final CurrencyData currencyData, final Long officeId) { + final Map thisTransactionData = new LinkedHashMap<>(); + + final SavingsAccountTransactionEnumData transactionType = SavingsEnumerations + .transactionType(this.transactionType.getId().intValue()); + + thisTransactionData.put("id", getId()); + thisTransactionData.put("officeId", officeId); + thisTransactionData.put("type", transactionType); + thisTransactionData.put("reversed", Boolean.valueOf(isReversed())); + thisTransactionData.put("date", getTransactionLocalDate()); + thisTransactionData.put("currency", currencyData); + thisTransactionData.put("amount", this.amount); + thisTransactionData.put("overdraftAmount", this.overdraftAmount); + + if (this.paymentDetailData != null) { + thisTransactionData.put("paymentTypeId", this.paymentDetailData.getPaymentType().getId()); + } + + /*** + * Sending data in a map, though in savings we currently expect a transaction to always repay a single charge + * (or may repay a part of a single charge too) + ***/ + if (!this.chargesPaidByData.isEmpty()) { + final List> savingsChargesPaidData = new ArrayList<>(); + for (final SavingsAccountChargesPaidByData chargePaidBy : this.chargesPaidByData) { + final Map savingChargePaidData = new LinkedHashMap<>(); + savingChargePaidData.put("chargeId", chargePaidBy.getSavingsAccountCharge()); + savingChargePaidData.put("isPenalty", chargePaidBy.getSavingsAccountCharge().isPenalty()); + savingChargePaidData.put("savingsChargeId", chargePaidBy.getSavingsAccountCharge().getId()); + savingChargePaidData.put("amount", chargePaidBy.getAmount()); + + savingsChargesPaidData.add(savingChargePaidData); + } + thisTransactionData.put("savingsChargesPaid", savingsChargesPaidData); + } + + if (this.taxDetails != null && !this.taxDetails.isEmpty()) { + final List> taxData = new ArrayList<>(); + for (final TaxDetailsData taxDetails : this.taxDetails) { + final Map taxDetailsData = new HashMap<>(); + taxDetailsData.put("amount", taxDetails.getAmount()); + if (taxDetails.getTaxComponent().getCreditAcount() != null) { + taxDetailsData.put("creditAccountId", taxDetails.getTaxComponent().getCreditAcount().getId()); + } + taxData.add(taxDetailsData); + } + thisTransactionData.put("taxDetails", taxData); + } + + return thisTransactionData; + } + + private SavingsAccountTransactionData(BigDecimal transactionAmount, LocalDate transactionDate, Long savingsAccountId, + SavingsAccountTransactionEnumData transactionType, boolean isReversed, String locale, boolean isManualTransaction) { + this.id = null; + this.transactionType = transactionType; + this.accountId = null; + this.accountNo = null; + this.date = transactionDate; + this.currency = null; + this.paymentDetailData = null; + this.amount = transactionAmount; + this.outstandingChargeAmount = null; + this.runningBalance = null; + this.reversed = isReversed; + this.transfer = null; + this.submittedOnDate = Date.from(transactionDate.atStartOfDay(ZoneId.systemDefault()).toInstant()); + this.interestedPostedAsOn = false; + this.rowIndex = null; + this.savingsAccountId = savingsAccountId; + this.dateFormat = null; + this.locale = locale; + this.transactionDate = transactionDate; + this.transactionAmount = transactionAmount; + this.paymentTypeId = null; + this.accountNumber = null; + this.checkNumber = null; + this.routingCode = null; + this.receiptNumber = null; + this.bankNumber = null; + this.paymentTypeOptions = null; + this.submittedByUsername = null; + this.note = null; + this.isManualTransaction = isManualTransaction; + } + private SavingsAccountTransactionData(BigDecimal transactionAmount, LocalDate transactionDate, Long paymentTypeId, String accountNumber, String checkNumber, String routingCode, String receiptNumber, String bankNumber, Long savingsAccountId, SavingsAccountTransactionEnumData transactionType, Integer rowIndex, String locale, String dateFormat) { @@ -110,6 +572,44 @@ public final class SavingsAccountTransactionData implements Serializable { this.paymentTypeOptions = null; this.submittedByUsername = null; this.note = null; + this.isManualTransaction = false; + } + + private SavingsAccountTransactionData(Integer id, BigDecimal transactionAmount, LocalDate transactionDate, Long paymentTypeId, + String accountNumber, String checkNumber, String routingCode, String receiptNumber, String bankNumber, Long savingsAccountId, + SavingsAccountTransactionEnumData transactionType, Integer rowIndex, String locale, String dateFormat, + BigDecimal cumulativeBalance) { + this.id = null; + this.transactionType = transactionType; + this.accountId = null; + this.accountNo = null; + this.date = null; + this.currency = null; + this.paymentDetailData = null; + this.amount = null; + this.outstandingChargeAmount = null; + this.runningBalance = null; + this.reversed = false; + this.submittedOnDate = null; + this.interestedPostedAsOn = false; + this.rowIndex = rowIndex; + this.savingsAccountId = savingsAccountId; + this.dateFormat = dateFormat; + this.locale = locale; + this.transactionDate = transactionDate; + this.transactionAmount = transactionAmount; + this.paymentTypeId = paymentTypeId; + this.accountNumber = accountNumber; + this.checkNumber = checkNumber; + this.routingCode = routingCode; + this.receiptNumber = receiptNumber; + this.bankNumber = bankNumber; + this.paymentTypeOptions = null; + this.submittedByUsername = null; + this.note = null; + this.cumulativeBalance = cumulativeBalance; + this.transfer = null; + this.isManualTransaction = false; } public Integer getRowIndex() { @@ -120,10 +620,128 @@ public final class SavingsAccountTransactionData implements Serializable { return savingsAccountId; } + public PaymentDetailData getPaymentDetailData() { + return this.paymentDetailData; + } + + public Long getId() { + return this.id; + } + public SavingsAccountTransactionEnumData getTransactionType() { return transactionType; } + public LocalDate getTransactionDate() { + return this.transactionDate; + } + + public LocalDate getDate() { + return this.date; + } + + public BigDecimal getAmount() { + return this.amount; + } + + public boolean isWithdrawal() { + return this.transactionType.isWithdrawal(); + } + + public boolean isInterestPosting() { + return this.transactionType.isInterestPosting() || this.transactionType.isOverDraftInterestPosting(); + } + + public boolean isManualTransaction() { + return this.isManualTransaction; + } + + public boolean isReversed() { + return this.reversed; + } + + public BigDecimal getRunningBalance() { + return this.runningBalance; + } + + public BigDecimal getCumulativeBalance() { + return this.cumulativeBalance; + } + + public LocalDate getBalanceEndDate() { + return this.balanceEndDate; + } + + public Date getSubmittedOnDate() { + return this.submittedOnDate; + } + + public boolean isWithHoldTaxAndNotReversed() { + return SavingsAccountTransactionType.fromInt(this.transactionType.getId().intValue()).isWithHoldTax() && isNotReversed(); + } + + public boolean isNotReversed() { + return !isReversed(); + } + + public BigDecimal getOverdraftAmount() { + return this.overdraftAmount; + } + + public boolean spansAnyPortionOf(final LocalDateInterval periodInterval) { + final LocalDateInterval balanceInterval = LocalDateInterval.create(getTransactionLocalDate(), getEndOfBalanceLocalDate()); + return balanceInterval.containsPortionOf(periodInterval); + } + + public static SavingsAccountTransactionData create(final Long id, final SavingsAccountTransactionEnumData transactionType, + final PaymentDetailData paymentDetailData, final Long savingsId, final String savingsAccountNo, final LocalDate date, + final CurrencyData currency, final BigDecimal amount, final BigDecimal outstandingChargeAmount, final BigDecimal runningBalance, + final boolean reversed, final LocalDate submittedOnDate, final boolean interestedPostedAsOn, final BigDecimal cumulativeBalance, + final LocalDate balanceEndDate) { + final Collection paymentTypeOptions = null; + return new SavingsAccountTransactionData(id, transactionType, paymentDetailData, savingsId, savingsAccountNo, date, currency, + amount, outstandingChargeAmount, runningBalance, reversed, submittedOnDate, paymentTypeOptions, interestedPostedAsOn, + cumulativeBalance, balanceEndDate); + } + + private SavingsAccountTransactionData(final Long id, final SavingsAccountTransactionEnumData transactionType, + final PaymentDetailData paymentDetailData, final Long savingsId, final String savingsAccountNo, final LocalDate date, + final CurrencyData currency, final BigDecimal amount, final BigDecimal outstandingChargeAmount, final BigDecimal runningBalance, + final boolean reversed, final LocalDate submittedOnDate, final Collection paymentTypeOptions, + final boolean interestedPostedAsOn, final BigDecimal cumulativeBalance, final LocalDate balanceEndDate) { + + this(id, transactionType, paymentDetailData, savingsId, savingsAccountNo, date, currency, amount, outstandingChargeAmount, + runningBalance, reversed, paymentTypeOptions, submittedOnDate, interestedPostedAsOn, cumulativeBalance, balanceEndDate); + } + + private SavingsAccountTransactionData(final Long id, final SavingsAccountTransactionEnumData transactionType, + final PaymentDetailData paymentDetailData, final Long savingsId, final String savingsAccountNo, final LocalDate date, + final CurrencyData currency, final BigDecimal amount, final BigDecimal outstandingChargeAmount, final BigDecimal runningBalance, + final boolean reversed, final Collection paymentTypeOptions, final LocalDate submittedOnDate, + final boolean interestedPostedAsOn, final BigDecimal cumulativeBalance, final LocalDate balanceEndDate) { + this.id = id; + this.transactionDate = date; + this.transactionType = transactionType; + this.paymentDetailData = paymentDetailData; + this.accountId = savingsId; + this.accountNo = savingsAccountNo; + this.date = date; + this.currency = currency; + this.amount = amount; + this.outstandingChargeAmount = outstandingChargeAmount; + this.runningBalance = runningBalance; + this.reversed = reversed; + this.paymentTypeOptions = paymentTypeOptions; + this.submittedOnDate = Date.from(submittedOnDate.atStartOfDay(ZoneId.systemDefault()).toInstant()); + this.interestedPostedAsOn = interestedPostedAsOn; + this.cumulativeBalance = cumulativeBalance; + this.transfer = null; + this.submittedByUsername = null; + this.note = null; + this.balanceEndDate = balanceEndDate; + this.isManualTransaction = false; + } + public static SavingsAccountTransactionData create(final Long id, final SavingsAccountTransactionEnumData transactionType, final PaymentDetailData paymentDetailData, final Long savingsId, final String savingsAccountNo, final LocalDate date, final CurrencyData currency, final BigDecimal amount, final BigDecimal outstandingChargeAmount, final BigDecimal runningBalance, @@ -146,6 +764,12 @@ public final class SavingsAccountTransactionData implements Serializable { interestedPostedAsOn, submittedByUsername, note); } + public static SavingsAccountTransactionData create(final Long id) { + final Collection paymentTypeOptions = null; + return new SavingsAccountTransactionData(id, null, null, null, null, null, null, null, null, null, false, null, paymentTypeOptions, + null, false, null, null); + } + public static SavingsAccountTransactionData template(final Long savingsId, final String savingsAccountNo, final LocalDate defaultLocalDate, final CurrencyData currency) { final Long id = null; @@ -182,7 +806,7 @@ public final class SavingsAccountTransactionData implements Serializable { final boolean interestedPostedAsOn, final String submittedByUsername, final String note) { this(id, transactionType, paymentDetailData, savingsId, savingsAccountNo, date, currency, amount, outstandingChargeAmount, - runningBalance, reversed, transfer, paymentTypeOptions, null, interestedPostedAsOn, submittedByUsername, note); + runningBalance, reversed, transfer, paymentTypeOptions, date, interestedPostedAsOn, submittedByUsername, note); } private SavingsAccountTransactionData(final Long id, final SavingsAccountTransactionEnumData transactionType, @@ -203,10 +827,47 @@ public final class SavingsAccountTransactionData implements Serializable { this.reversed = reversed; this.transfer = transfer; this.paymentTypeOptions = paymentTypeOptions; - this.submittedOnDate = submittedOnDate; + if (submittedOnDate != null) { + this.submittedOnDate = Date.from(submittedOnDate.atStartOfDay(ZoneId.systemDefault()).toInstant()); + } else { + this.submittedOnDate = null; + } + this.interestedPostedAsOn = interestedPostedAsOn; this.submittedByUsername = submittedByUsername; this.note = note; + this.isManualTransaction = false; + } + + private SavingsAccountTransactionData(final Long id, final SavingsAccountTransactionEnumData transactionType, + final PaymentDetailData paymentDetailData, final Long savingsId, final String savingsAccountNo, final LocalDate date, + final CurrencyData currency, final BigDecimal amount, final BigDecimal outstandingChargeAmount, final BigDecimal runningBalance, + final boolean reversed, final AccountTransferData transfer, final Collection paymentTypeOptions, + final LocalDate submittedOnDate, final boolean interestedPostedAsOn, final BigDecimal cumulativeBalance) { + this.id = id; + this.transactionType = transactionType; + this.paymentDetailData = paymentDetailData; + this.accountId = savingsId; + this.accountNo = savingsAccountNo; + this.date = date; + this.currency = currency; + this.amount = amount; + this.outstandingChargeAmount = outstandingChargeAmount; + this.runningBalance = runningBalance; + this.reversed = reversed; + this.transfer = transfer; + this.paymentTypeOptions = paymentTypeOptions; + if (submittedOnDate != null) { + this.submittedOnDate = Date.from(submittedOnDate.atStartOfDay(ZoneId.systemDefault()).toInstant()); + } else { + this.submittedOnDate = null; + } + + this.interestedPostedAsOn = interestedPostedAsOn; + this.submittedByUsername = null; + this.note = null; + this.cumulativeBalance = cumulativeBalance; + this.isManualTransaction = false; } public static SavingsAccountTransactionData withWithDrawalTransactionDetails( @@ -225,4 +886,9 @@ public final class SavingsAccountTransactionData implements Serializable { savingsAccountTransactionData.interestedPostedAsOn, savingsAccountTransactionData.submittedByUsername, savingsAccountTransactionData.note); } + + public void setId(final Long id) { + this.id = id; + this.modifiedId = id; + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionEnumData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionEnumData.java index 61684354b..46157fcc2 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionEnumData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionEnumData.java @@ -100,6 +100,10 @@ public class SavingsAccountTransactionEnumData implements Serializable { return this.interestPosting; } + public boolean isIncomeFromInterest() { + return this.overdraftInterest; + } + public boolean isFeeDeduction() { return this.feeDeduction; } @@ -152,4 +156,24 @@ public class SavingsAccountTransactionEnumData implements Serializable { return this.amountRelease; } + public boolean isOverDraftInterestPosting() { + return this.overdraftInterest; + } + + public boolean isChargeTransaction() { + return isPayCharge() || isWithdrawalFee() || isAnnualFee(); + } + + public boolean isAnnualFee() { + return this.value.equals(SavingsAccountTransactionType.ANNUAL_FEE.getValue().toString()); + } + + public boolean isPayCharge() { + return this.value.equals(SavingsAccountTransactionType.PAY_CHARGE.getValue().toString()); + } + + public boolean isWithdrawalFee() { + return this.value.equals(SavingsAccountTransactionType.WITHDRAWAL_FEE.getValue().toString()); + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsProductData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsProductData.java index ccc225bd0..c9ae6b147 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsProductData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsProductData.java @@ -339,6 +339,67 @@ public final class SavingsProductData implements Serializable { daysToDormancy, daysToEscheat, accountMappingForPayment); } + public static SavingsProductData createForInterestPosting(final Long id, final EnumOptionData accountingRule) { + return new SavingsProductData(id, accountingRule); + } + + private SavingsProductData(final Long id, final EnumOptionData accountingRule) { + this.id = id; + this.name = null; + this.shortName = null; + this.description = null; + this.currency = null; + this.nominalAnnualInterestRate = null; + this.interestCompoundingPeriodType = null; + this.interestPostingPeriodType = null; + this.interestCalculationType = null; + this.interestCalculationDaysInYearType = null; + this.accountingRule = accountingRule; + this.minRequiredOpeningBalance = null; + this.lockinPeriodFrequency = null; + this.lockinPeriodFrequencyType = null; + this.withdrawalFeeForTransfers = false; + + this.currencyOptions = null; + this.interestCompoundingPeriodTypeOptions = null; + this.interestPostingPeriodTypeOptions = null; + this.interestCalculationTypeOptions = null; + this.interestCalculationDaysInYearTypeOptions = null; + this.lockinPeriodFrequencyTypeOptions = null; + this.withdrawalFeeTypeOptions = null; + + this.paymentTypeOptions = null; + this.accountingMappingOptions = null; + this.accountingRuleOptions = null; + this.accountingMappings = null; + + this.paymentChannelToFundSourceMappings = null; + + this.charges = null;// charges associated with Savings product + this.chargeOptions = null;// charges available for adding to + // Savings product + this.penaltyOptions = null;// penalties available for adding + // to Savings product + + this.feeToIncomeAccountMappings = null; + this.penaltyToIncomeAccountMappings = null; + this.allowOverdraft = false; + this.overdraftLimit = null; + this.minRequiredBalance = null; + this.enforceMinRequiredBalance = false; + this.minBalanceForInterestCalculation = null; + this.nominalAnnualInterestRateOverdraft = null; + this.minOverdraftForInterestCalculation = null; + this.taxGroup = null; + this.withHoldTax = false; + this.taxGroupOptions = null; + this.isDormancyTrackingActive = null; + this.daysToInactive = null; + this.daysToDormancy = null; + this.daysToEscheat = null; + this.accountMappingForPayment = null; + } + private SavingsProductData(final Long id, final String name, final String shortName, final String description, final CurrencyData currency, final BigDecimal nominalAnnualInterestRate, final EnumOptionData interestCompoundingPeriodType, final EnumOptionData interestPostingPeriodType, final EnumOptionData interestCalculationType, @@ -509,4 +570,21 @@ public final class SavingsProductData implements Serializable { public boolean isWithdrawalFeeForTransfers() { return withdrawalFeeForTransfers; } + + public boolean isCashBasedAccountingEnabled() { + return AccountingRuleType.CASH_BASED.getValue().toString().equals(this.accountingRule.getValue()); + } + + public boolean isAccrualBasedAccountingEnabled() { + return isUpfrontAccrualAccounting() || isPeriodicAccrualAccounting(); + } + + public boolean isUpfrontAccrualAccounting() { + return AccountingRuleType.ACCRUAL_UPFRONT.getValue().toString().equals(this.accountingRule.getValue()); + } + + public boolean isPeriodicAccrualAccounting() { + return AccountingRuleType.ACCRUAL_PERIODIC.getValue().toString().equals(this.accountingRule.getValue()); + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java index 94192df2f..da03341f7 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java @@ -1003,7 +1003,7 @@ public class SavingsAccount extends AbstractPersistableCustom { if (overdraftAmount.isGreaterThanZero()) { accountTransaction.updateOverdraftAmount(overdraftAmount.getAmount()); } - accountTransaction.updateRunningBalance(runningBalance); + // accountTransaction.updateRunningBalance(runningBalance); if (backdatedTxnsAllowedTill) { addTransactionToExisting(accountTransaction); } else { @@ -1108,6 +1108,11 @@ public class SavingsAccount extends AbstractPersistableCustom { this.sub_status = SavingsAccountSubStatusEnum.NONE.getValue(); } + if (backdatedTxnsAllowedTill) { + this.summary.updateSummaryWithPivotConfig(this.currency, this.savingsAccountTransactionSummaryWrapper, transaction, + this.savingsAccountTransactions); + } + return transaction; } @@ -1119,10 +1124,18 @@ public class SavingsAccount extends AbstractPersistableCustom { return activationLocalDate; } + public AppUser getActivatedBy() { + return this.activatedBy; + } + public LocalDate getWithdrawnOnDate() { return withdrawnOnDate == null ? null : LocalDate.ofInstant(withdrawnOnDate.toInstant(), DateUtils.getDateTimeZoneOfTenant()); } + 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() { @@ -1950,6 +1963,10 @@ public class SavingsAccount extends AbstractPersistableCustom { return gsim; } + public Long getSavingsProductId() { + return this.savingsProduct().getId(); + } + public void setGsim(GroupSavingsIndividualMonitoring gsim) { this.gsim = gsim; } @@ -2063,6 +2080,10 @@ public class SavingsAccount extends AbstractPersistableCustom { return rejectedOnDate == null ? null : LocalDate.ofInstant(rejectedOnDate.toInstant(), DateUtils.getDateTimeZoneOfTenant()); } + public AppUser getRejectedBy() { + return this.rejectedBy; + } + public void removeSavingsOfficer(final LocalDate unassignDate) { final SavingsOfficerAssignmentHistory latestHistoryRecord = findLatestIncompleteHistoryRecord(); @@ -2169,6 +2190,18 @@ public class SavingsAccount extends AbstractPersistableCustom { 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; } @@ -2775,6 +2808,10 @@ public class SavingsAccount extends AbstractPersistableCustom { return this.closedOnDate == null ? null : LocalDate.ofInstant(this.closedOnDate.toInstant(), DateUtils.getDateTimeZoneOfTenant()); } + public AppUser getClosedBy() { + return this.closedBy; + } + public SavingsAccountSummary getSummary() { return this.summary; } @@ -3213,6 +3250,14 @@ public class SavingsAccount extends AbstractPersistableCustom { } } + public AppUser getSubmittedBy() { + return this.submittedBy; + } + + public AppUser getApprovedBy() { + return this.approvedBy; + } + public boolean allowDeposit() { return true; } @@ -3665,4 +3710,68 @@ public class SavingsAccount extends AbstractPersistableCustom { 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 Date getLockedInUntilDate() { + return this.lockedInUntilDate; + } + + public Integer getDepositType() { + return this.depositType; + } + + public BigDecimal getMinRequiredBalance() { + return this.minRequiredBalance; + } + + public boolean isEnforceMinRequiredBalance() { + return this.enforceMinRequiredBalance; + } + + public BigDecimal getMinBalanceForInterestCalculation() { + return this.minBalanceForInterestCalculation; + } + + public int getVersion() { + return this.version; + } + + public boolean isWithHoldTax() { + return this.withHoldTax; + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountAssembler.java index 17eab631e..0e0839861 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountAssembler.java @@ -73,6 +73,7 @@ import org.apache.fineract.portfolio.savings.SavingsInterestCalculationDaysInYea import org.apache.fineract.portfolio.savings.SavingsInterestCalculationType; import org.apache.fineract.portfolio.savings.SavingsPeriodFrequencyType; import org.apache.fineract.portfolio.savings.SavingsPostingInterestPeriodType; +import org.apache.fineract.portfolio.savings.data.SavingsAccountData; import org.apache.fineract.portfolio.savings.exception.SavingsProductNotFoundException; import org.apache.fineract.useradministration.domain.AppUser; import org.slf4j.Logger; @@ -361,6 +362,18 @@ public class SavingsAccountAssembler { return account; } + public SavingsAccountData assembleSavings(final SavingsAccountData account) { + + // Update last running balance on account level + if (account.getTransactions() != null && account.getTransactions().size() != 0) { + account.getSummary().setRunningBalanceOnPivotDate(account.getTransactions().get(account.getTransactions().size() - 1) + .getRunningBalance(account.getCurrency()).getAmount()); + } + + account.setHelpers(this.savingsAccountTransactionSummaryWrapper, this.savingsHelper); + return account; + } + public boolean getPivotConfigStatus() { return this.configurationDomainService.retrievePivotDateConfig(); } @@ -425,4 +438,8 @@ public class SavingsAccountAssembler { public void assignSavingAccountHelpers(final SavingsAccount savingsAccount) { savingsAccount.setHelpers(this.savingsAccountTransactionSummaryWrapper, this.savingsHelper); } + + public void assignSavingAccountHelpers(final SavingsAccountData savingsAccountData) { + savingsAccountData.setHelpers(this.savingsAccountTransactionSummaryWrapper, this.savingsHelper); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountChargesPaidByData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountChargesPaidByData.java new file mode 100644 index 000000000..2ed84b522 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountChargesPaidByData.java @@ -0,0 +1,64 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.savings.domain; + +import java.io.Serializable; +import java.math.BigDecimal; +import org.apache.fineract.portfolio.savings.data.SavingsAccountChargeData; + +public class SavingsAccountChargesPaidByData implements Serializable { + + private final Long chargeId; + private final BigDecimal amount; + private SavingsAccountChargeData savingsAccountChargeData; + + public SavingsAccountChargesPaidByData(final Long chargeId, final BigDecimal amount) { + this.chargeId = chargeId; + this.amount = amount; + } + + public BigDecimal getAmount() { + return this.amount; + } + + public Long getChargeId() { + return this.chargeId; + } + + public void setSavingsAccountChargeData(final SavingsAccountChargeData savingsAccountChargeData) { + this.savingsAccountChargeData = savingsAccountChargeData; + } + + public static SavingsAccountChargesPaidByData instance(final Long savingsAccountChargeId, final BigDecimal amount) { + return new SavingsAccountChargesPaidByData(savingsAccountChargeId, amount); + } + + public boolean isFeeCharge() { + return (this.savingsAccountChargeData == null) ? false : this.savingsAccountChargeData.isFeeCharge(); + } + + public boolean isPenaltyCharge() { + return (this.savingsAccountChargeData == null) ? false : this.savingsAccountChargeData.isPenaltyCharge(); + } + + public SavingsAccountChargeData getSavingsAccountCharge() { + return this.savingsAccountChargeData; + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepositoryWrapper.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepositoryWrapper.java index f031a0c6b..3e635118e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepositoryWrapper.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepositoryWrapper.java @@ -170,4 +170,5 @@ public class SavingsAccountRepositoryWrapper { account.loadLazyCollections(); } } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountSummary.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountSummary.java index 9bddc61fd..67f191322 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountSummary.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountSummary.java @@ -286,4 +286,39 @@ public final class SavingsAccountSummary { return this.runningBalanceOnInterestPostingTillDate; } + public BigDecimal getTotalWithdrawals() { + return this.totalWithdrawals; + } + + public BigDecimal getTotalDeposits() { + return this.totalDeposits; + } + + public BigDecimal getTotalWithdrawalFees() { + return this.totalWithdrawalFees; + } + + public BigDecimal getTotalFeeCharge() { + return this.totalFeeCharge; + } + + public BigDecimal getTotalPenaltyCharge() { + return this.totalPenaltyCharge; + } + + public BigDecimal getTotalAnnualFees() { + return this.totalAnnualFees; + } + + public BigDecimal getTotalInterestEarned() { + return this.totalInterestEarned; + } + + public BigDecimal getTotalOverdraftInterestDerived() { + return this.totalOverdraftInterestDerived; + } + + public BigDecimal getTotalWithholdTax() { + return this.totalWithholdTax; + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransaction.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransaction.java index b2b71a28f..c190ef528 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransaction.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransaction.java @@ -201,6 +201,14 @@ public final class SavingsAccountTransaction extends AbstractPersistableCustom { isReversed, appUser, isManualTransaction); } + public static SavingsAccountTransaction from(final Integer transactionTypeEnum, final LocalDate transactionDate, + final BigDecimal amount, final boolean isReversed, final BigDecimal runningBalance, final BigDecimal cumulativeBalance, + final LocalDate balanceEndDate, final Integer balanceNumberOfDays, final BigDecimal overdraftAmount, + final LocalDate createdDate, final boolean isManualTransaction, final Long releaseIdOfHoldAmountTransaction) { + return new SavingsAccountTransaction(transactionTypeEnum, transactionDate, amount, isReversed, runningBalance, cumulativeBalance, + balanceEndDate, balanceNumberOfDays, overdraftAmount, createdDate, isManualTransaction, releaseIdOfHoldAmountTransaction); + } + public static SavingsAccountTransaction waiver(final SavingsAccount savingsAccount, final Office office, final LocalDate date, final Money amount, final AppUser appUser) { final boolean isReversed = false; @@ -301,6 +309,24 @@ public final class SavingsAccountTransaction extends AbstractPersistableCustom { this.isManualTransaction = isManualTransaction; } + private SavingsAccountTransaction(final Integer transactionTypeEnum, final LocalDate transactionDate, final BigDecimal amount, + final boolean isReversed, final BigDecimal runningBalance, final BigDecimal cumulativeBalance, final LocalDate balanceEndDate, + final Integer balanceNumberOfDays, final BigDecimal overdraftAmount, final LocalDate createdDate, + final boolean isManualTransaction, final Long releaseIdOfHoldAmountTransaction) { + this.typeOf = transactionTypeEnum; + this.dateOf = Date.from(transactionDate.atStartOfDay(ZoneId.systemDefault()).toInstant()); + this.amount = amount; + this.reversed = isReversed; + this.runningBalance = runningBalance; + this.cumulativeBalance = cumulativeBalance; + this.balanceEndDate = Date.from(balanceEndDate.atStartOfDay(ZoneId.systemDefault()).toInstant()); + this.balanceNumberOfDays = balanceNumberOfDays; + this.overdraftAmount = overdraftAmount; + this.createdDate = Date.from(createdDate.atStartOfDay(ZoneId.systemDefault()).toInstant()); + this.isManualTransaction = isManualTransaction; + this.releaseIdOfHoldAmountTransaction = releaseIdOfHoldAmountTransaction; + } + public static SavingsAccountTransaction holdAmount(final SavingsAccount savingsAccount, final Office office, final PaymentDetail paymentDetail, final LocalDate date, final Money amount, Date createdDate, final AppUser appUser) { final boolean isReversed = false; @@ -809,4 +835,32 @@ public final class SavingsAccountTransaction extends AbstractPersistableCustom { public boolean isAmountOnHoldNotReleased() { return (isAmountOnHold() && getReleaseIdOfHoldAmountTransaction() == null); } + + public Long getOfficeId() { + return this.office.getId(); + } + + public Date getBalanceEndDate() { + return this.balanceEndDate; + } + + public BigDecimal getCumulativeBalance() { + return this.cumulativeBalance; + } + + public Integer getBalanceNumberOfDays() { + return this.balanceNumberOfDays; + } + + public Long getAppUserId() { + return this.appUser.getId(); + } + + public Date getCreatedDate() { + return this.createdDate; + } + + public boolean getIsManualTransaction() { + return this.isManualTransaction; + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransactionDataComparator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransactionDataComparator.java new file mode 100644 index 000000000..223b819b1 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransactionDataComparator.java @@ -0,0 +1,44 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.savings.domain; + +import java.util.Comparator; +import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData; + +public class SavingsAccountTransactionDataComparator implements Comparator { + + @Override + public int compare(final SavingsAccountTransactionData o1, final SavingsAccountTransactionData o2) { + int compareResult = 0; + + final int comparsion = o1.getTransactionDate().compareTo(o2.getLastTransactionDate()); + if (comparsion == 0) { + compareResult = o1.getSubmittedOnDate().compareTo(o2.getSubmittedOnDate()); + if (compareResult == 0 && o1.getId() != null && o2.getId() != null) { + compareResult = o1.getId().compareTo(o2.getId()); + } else { + compareResult = comparsion; + } + } else { + compareResult = comparsion; + } + return compareResult; + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransactionSummaryWrapper.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransactionSummaryWrapper.java index 03ffb8edb..796653c83 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransactionSummaryWrapper.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransactionSummaryWrapper.java @@ -20,8 +20,10 @@ package org.apache.fineract.portfolio.savings.domain; import java.math.BigDecimal; import java.util.List; +import org.apache.fineract.organisation.monetary.data.CurrencyData; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData; import org.springframework.stereotype.Component; /** @@ -41,6 +43,16 @@ public final class SavingsAccountTransactionSummaryWrapper { return total.getAmountDefaultedToNullIfZero(); } + public BigDecimal calculateTotalDeposits(final CurrencyData currency, final List transactions) { + Money total = Money.zero(currency); + for (final SavingsAccountTransactionData transaction : transactions) { + if (transaction.isDepositAndNotReversed() || transaction.isDividendPayoutAndNotReversed()) { + total = total.plus(transaction.getAmount()); + } + } + return total.getAmountDefaultedToNullIfZero(); + } + public BigDecimal calculateTotalWithdrawals(final MonetaryCurrency currency, final List transactions) { Money total = Money.zero(currency); for (final SavingsAccountTransaction transaction : transactions) { @@ -51,6 +63,16 @@ public final class SavingsAccountTransactionSummaryWrapper { return total.getAmountDefaultedToNullIfZero(); } + public BigDecimal calculateTotalWithdrawals(final CurrencyData currency, final List transactions) { + Money total = Money.zero(currency); + for (final SavingsAccountTransactionData transaction : transactions) { + if (transaction.isWithdrawal() && transaction.isNotReversed()) { + total = total.plus(transaction.getAmount()); + } + } + return total.getAmountDefaultedToNullIfZero(); + } + public BigDecimal calculateTotalInterestPosted(final MonetaryCurrency currency, final List transactions) { Money total = Money.zero(currency); for (final SavingsAccountTransaction transaction : transactions) { @@ -61,6 +83,16 @@ public final class SavingsAccountTransactionSummaryWrapper { return total.getAmountDefaultedToNullIfZero(); } + public BigDecimal calculateTotalInterestPosted(final CurrencyData currency, final List transactions) { + Money total = Money.zero(currency); + for (final SavingsAccountTransactionData transaction : transactions) { + if (transaction.isInterestPostingAndNotReversed() && transaction.isNotReversed()) { + total = total.plus(transaction.getAmount()); + } + } + return total.getAmountDefaultedToNullIfZero(); + } + public BigDecimal calculateTotalInterestPosted(final MonetaryCurrency currency, final BigDecimal currentInterestPosted, final List savingsAccountTransactions) { Money total = Money.of(currency, currentInterestPosted); @@ -82,6 +114,16 @@ public final class SavingsAccountTransactionSummaryWrapper { return total.getAmountDefaultedToNullIfZero(); } + public BigDecimal calculateTotalWithdrawalFees(final CurrencyData currency, final List transactions) { + Money total = Money.zero(currency); + for (final SavingsAccountTransactionData transaction : transactions) { + if (transaction.isWithdrawalFeeAndNotReversed() && transaction.isNotReversed()) { + total = total.plus(transaction.getAmount()); + } + } + return total.getAmountDefaultedToNullIfZero(); + } + public BigDecimal calculateTotalAnnualFees(final MonetaryCurrency currency, final List transactions) { Money total = Money.zero(currency); for (final SavingsAccountTransaction transaction : transactions) { @@ -92,6 +134,16 @@ public final class SavingsAccountTransactionSummaryWrapper { return total.getAmountDefaultedToNullIfZero(); } + public BigDecimal calculateTotalAnnualFees(final CurrencyData currency, final List transactions) { + Money total = Money.zero(currency); + for (final SavingsAccountTransactionData transaction : transactions) { + if (transaction.isAnnualFeeAndNotReversed() && transaction.isNotReversed()) { + total = total.plus(transaction.getAmount()); + } + } + return total.getAmountDefaultedToNullIfZero(); + } + public BigDecimal calculateTotalFeesCharge(final MonetaryCurrency currency, final List transactions) { Money total = Money.zero(currency); for (final SavingsAccountTransaction transaction : transactions) { @@ -102,6 +154,16 @@ public final class SavingsAccountTransactionSummaryWrapper { return total.getAmountDefaultedToNullIfZero(); } + public BigDecimal calculateTotalFeesCharge(final CurrencyData currency, final List transactions) { + Money total = Money.zero(currency); + for (final SavingsAccountTransactionData transaction : transactions) { + if (transaction.isFeeChargeAndNotReversed()) { + total = total.plus(transaction.getAmount()); + } + } + return total.getAmountDefaultedToNullIfZero(); + } + public BigDecimal calculateTotalFeesChargeWaived(final MonetaryCurrency currency, final List transactions) { Money total = Money.zero(currency); for (final SavingsAccountTransaction transaction : transactions) { @@ -112,6 +174,16 @@ public final class SavingsAccountTransactionSummaryWrapper { return total.getAmountDefaultedToNullIfZero(); } + public BigDecimal calculateTotalFeesChargeWaived(final CurrencyData currency, final List transactions) { + Money total = Money.zero(currency); + for (final SavingsAccountTransactionData transaction : transactions) { + if (transaction.isWaiveFeeChargeAndNotReversed()) { + total = total.plus(transaction.getAmount()); + } + } + return total.getAmountDefaultedToNullIfZero(); + } + public BigDecimal calculateTotalPenaltyCharge(final MonetaryCurrency currency, final List transactions) { Money total = Money.zero(currency); for (final SavingsAccountTransaction transaction : transactions) { @@ -122,6 +194,16 @@ public final class SavingsAccountTransactionSummaryWrapper { return total.getAmountDefaultedToNullIfZero(); } + public BigDecimal calculateTotalPenaltyCharge(final CurrencyData currency, final List transactions) { + Money total = Money.zero(currency); + for (final SavingsAccountTransactionData transaction : transactions) { + if (transaction.isPenaltyChargeAndNotReversed()) { + total = total.plus(transaction.getAmount()); + } + } + return total.getAmountDefaultedToNullIfZero(); + } + public BigDecimal calculateTotalPenaltyChargeWaived(final MonetaryCurrency currency, final List transactions) { Money total = Money.zero(currency); @@ -133,6 +215,17 @@ public final class SavingsAccountTransactionSummaryWrapper { return total.getAmountDefaultedToNullIfZero(); } + public BigDecimal calculateTotalPenaltyChargeWaived(final CurrencyData currency, + final List transactions) { + Money total = Money.zero(currency); + for (final SavingsAccountTransactionData transaction : transactions) { + if (transaction.isWaivePenaltyChargeAndNotReversed()) { + total = total.plus(transaction.getAmount()); + } + } + return total.getAmountDefaultedToNullIfZero(); + } + public BigDecimal calculateTotalOverdraftInterest(MonetaryCurrency currency, BigDecimal overdraftPosted, List transactions) { Money total = Money.of(currency, overdraftPosted); @@ -154,6 +247,16 @@ public final class SavingsAccountTransactionSummaryWrapper { return total.getAmountDefaultedToNullIfZero(); } + public BigDecimal calculateTotalOverdraftInterest(CurrencyData currency, List transactions) { + Money total = Money.zero(currency); + for (final SavingsAccountTransactionData transaction : transactions) { + if (transaction.isOverdraftInterestAndNotReversed()) { + total = total.plus(transaction.getAmount()); + } + } + return total.getAmountDefaultedToNullIfZero(); + } + public BigDecimal calculateTotalWithholdTaxWithdrawal(MonetaryCurrency currency, List transactions) { Money total = Money.zero(currency); for (final SavingsAccountTransaction transaction : transactions) { @@ -163,4 +266,14 @@ public final class SavingsAccountTransactionSummaryWrapper { } return total.getAmountDefaultedToNullIfZero(); } + + public BigDecimal calculateTotalWithholdTaxWithdrawal(CurrencyData currency, List transactions) { + Money total = Money.zero(currency); + for (final SavingsAccountTransactionData transaction : transactions) { + if (transaction.isWithHoldTaxAndNotReversed()) { + total = total.plus(transaction.getAmount()); + } + } + return total.getAmountDefaultedToNullIfZero(); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/PostingPeriod.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/PostingPeriod.java index 4d5a5ff02..98dcf4978 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/PostingPeriod.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/interest/PostingPeriod.java @@ -32,6 +32,7 @@ import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.portfolio.savings.SavingsCompoundingInterestPeriodType; import org.apache.fineract.portfolio.savings.SavingsInterestCalculationType; +import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData; import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction; public final class PostingPeriod { @@ -96,6 +97,7 @@ public final class PostingPeriod { boolean interestTransfered = false; Money openingDayBalance = periodStartingBalance; Money closeOfDayBalance = openingDayBalance; + for (final SavingsAccountTransaction transaction : orderedListOfTransactions) { if (transaction.fallsWithin(periodInterval)) { @@ -155,6 +157,79 @@ public final class PostingPeriod { minOverdraftForInterestCalculation, isUserPosting, financialYearBeginningMonth); } + public static PostingPeriod createFromDTO(final LocalDateInterval periodInterval, final Money periodStartingBalance, + final List orderedListOfTransactions, final MonetaryCurrency currency, + final SavingsCompoundingInterestPeriodType interestCompoundingPeriodType, + final SavingsInterestCalculationType interestCalculationType, final BigDecimal interestRateAsFraction, final long daysInYear, + final LocalDate upToInterestCalculationDate, Collection interestPostTransactions, boolean isInterestTransfer, + final Money minBalanceForInterestCalculation, final boolean isSavingsInterestPostingAtCurrentPeriodEnd, + final BigDecimal overdraftInterestRateAsFraction, final Money minOverdraftForInterestCalculation, boolean isUserPosting, + int financialYearBeginningMonth) { + + final List accountEndOfDayBalances = new ArrayList<>(); + boolean interestTransfered = false; + Money openingDayBalance = periodStartingBalance; + Money closeOfDayBalance = openingDayBalance; + + for (final SavingsAccountTransactionData transaction : orderedListOfTransactions) { + + if (transaction.fallsWithin(periodInterval)) { + // the balance of the transaction falls entirely within this + // period so no need to do any cropping/bounding + final EndOfDayBalance endOfDayBalance = transaction.toEndOfDayBalance(openingDayBalance); + accountEndOfDayBalances.add(endOfDayBalance); + + openingDayBalance = endOfDayBalance.closingBalance(); + + } else if (transaction.spansAnyPortionOf(periodInterval)) { + final EndOfDayBalance endOfDayBalance = transaction.toEndOfDayBalanceBoundedBy(openingDayBalance, periodInterval); + accountEndOfDayBalances.add(endOfDayBalance); + + closeOfDayBalance = endOfDayBalance.closingBalance(); + openingDayBalance = closeOfDayBalance; + } + + // this check is to make sure to add interest if withdrawal is + // happened for already + if (transaction.occursOn(periodInterval.endDate().plusDays(1))) { + if (transaction.getId() == null) { + interestTransfered = isInterestTransfer; + } else if (interestPostTransactions.contains(transaction.getId())) { + interestTransfered = true; + } + } + + } + + if (accountEndOfDayBalances.isEmpty()) { + LocalDate balanceStartDate = periodInterval.startDate(); + LocalDate balanceEndDate = periodInterval.endDate(); + Integer numberOfDaysOfBalance = periodInterval.daysInPeriodInclusiveOfEndDate(); + + if (balanceEndDate.isAfter(upToInterestCalculationDate)) { + balanceEndDate = upToInterestCalculationDate; + final LocalDateInterval spanOfBalance = LocalDateInterval.create(balanceStartDate, balanceEndDate); + numberOfDaysOfBalance = spanOfBalance.daysInPeriodInclusiveOfEndDate(); + } + + final EndOfDayBalance endOfDayBalance = EndOfDayBalance.from(balanceStartDate, openingDayBalance, closeOfDayBalance, + numberOfDaysOfBalance); + + accountEndOfDayBalances.add(endOfDayBalance); + + closeOfDayBalance = endOfDayBalance.closingBalance(); + openingDayBalance = closeOfDayBalance; + } + + final List compoundingPeriods = compoundingPeriodsInPostingPeriod(periodInterval, interestCompoundingPeriodType, + accountEndOfDayBalances, upToInterestCalculationDate, financialYearBeginningMonth); + + return new PostingPeriod(periodInterval, currency, periodStartingBalance, openingDayBalance, interestCompoundingPeriodType, + interestCalculationType, interestRateAsFraction, daysInYear, compoundingPeriods, interestTransfered, + minBalanceForInterestCalculation, isSavingsInterestPostingAtCurrentPeriodEnd, overdraftInterestRateAsFraction, + minOverdraftForInterestCalculation, isUserPosting, financialYearBeginningMonth); + } + private PostingPeriod(final LocalDateInterval periodInterval, final MonetaryCurrency currency, final Money openingBalance, final Money closingBalance, final SavingsCompoundingInterestPeriodType interestCompoundingType, final SavingsInterestCalculationType interestCalculationType, final BigDecimal interestRateAsFraction, final long daysInYear, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountInterestPostingService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountInterestPostingService.java new file mode 100644 index 000000000..26c05c356 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountInterestPostingService.java @@ -0,0 +1,31 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.savings.service; + +import java.math.MathContext; +import java.time.LocalDate; +import org.apache.fineract.portfolio.savings.data.SavingsAccountData; + +public interface SavingsAccountInterestPostingService { + + SavingsAccountData postInterest(MathContext mc, LocalDate interestPostingUpToDate, boolean isInterestTransfer, + boolean isSavingsInterestPostingAtCurrentPeriodEnd, Integer financialYearBeginningMonth, LocalDate postInterestOnDate, + boolean backdatedTxnsAllowedTill, SavingsAccountData savingsAccountData); + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountInterestPostingServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountInterestPostingServiceImpl.java new file mode 100644 index 000000000..dd660ffad --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountInterestPostingServiceImpl.java @@ -0,0 +1,564 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.savings.service; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import org.apache.fineract.infrastructure.core.data.ApiParameterError; +import org.apache.fineract.infrastructure.core.domain.LocalDateInterval; +import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.portfolio.savings.DepositAccountType; +import org.apache.fineract.portfolio.savings.SavingsCompoundingInterestPeriodType; +import org.apache.fineract.portfolio.savings.SavingsInterestCalculationDaysInYearType; +import org.apache.fineract.portfolio.savings.SavingsInterestCalculationType; +import org.apache.fineract.portfolio.savings.SavingsPostingInterestPeriodType; +import org.apache.fineract.portfolio.savings.data.SavingsAccountData; +import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountChargesPaidByData; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransactionDataComparator; +import org.apache.fineract.portfolio.savings.domain.SavingsHelper; +import org.apache.fineract.portfolio.savings.domain.interest.PostingPeriod; +import org.apache.fineract.portfolio.tax.data.TaxComponentData; +import org.apache.fineract.portfolio.tax.service.TaxUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class SavingsAccountInterestPostingServiceImpl implements SavingsAccountInterestPostingService { + + private final SavingsHelper savingsHelper; + + @Autowired + public SavingsAccountInterestPostingServiceImpl(final SavingsHelper savingsHelper) { + this.savingsHelper = savingsHelper; + } + + @Override + public SavingsAccountData postInterest(final MathContext mc, final LocalDate interestPostingUpToDate, final boolean isInterestTransfer, + final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth, + final LocalDate postInterestOnDate, final boolean backdatedTxnsAllowedTill, final SavingsAccountData savingsAccountData) { + + Money interestPostedToDate = Money.zero(savingsAccountData.currency()); + LocalDate startInterestDate = getStartInterestCalculationDate(savingsAccountData); + + if (backdatedTxnsAllowedTill && savingsAccountData.getSummary().getInterestPostedTillDate() != null) { + interestPostedToDate = Money.of(savingsAccountData.currency(), savingsAccountData.getSummary().getTotalInterestPosted()); + SavingsAccountTransactionData savingsAccountTransactionData = retrieveLastTransactions(savingsAccountData); + Date lastTransactionDate = Date.from( + savingsAccountTransactionData.getLastTransactionDate().atStartOfDay(DateUtils.getDateTimeZoneOfTenant()).toInstant()); + savingsAccountData.setStartInterestCalculationDate(lastTransactionDate); + } else { + savingsAccountData.setStartInterestCalculationDate( + Date.from(startInterestDate.atStartOfDay(DateUtils.getDateTimeZoneOfTenant()).toInstant())); + } + + final List postingPeriods = calculateInterestUsing(mc, interestPostingUpToDate, isInterestTransfer, + isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, + savingsAccountData); + + boolean recalucateDailyBalanceDetails = false; + boolean applyWithHoldTax = isWithHoldTaxApplicableForInterestPosting(savingsAccountData); + final List withholdTransactions = new ArrayList<>(); + + withholdTransactions.addAll(findWithHoldSavingsTransactionsWithPivotConfig(savingsAccountData)); + + for (final PostingPeriod interestPostingPeriod : postingPeriods) { + + final LocalDate interestPostingTransactionDate = interestPostingPeriod.dateOfPostingTransaction(); + final Money interestEarnedToBePostedForPeriod = interestPostingPeriod.getInterestEarned(); + + if (!interestPostingTransactionDate.isAfter(interestPostingUpToDate)) { + interestPostedToDate = interestPostedToDate.plus(interestEarnedToBePostedForPeriod); + + final SavingsAccountTransactionData postingTransaction = findInterestPostingTransactionFor(interestPostingTransactionDate, + savingsAccountData); + + if (postingTransaction == null) { + SavingsAccountTransactionData newPostingTransaction; + if (interestEarnedToBePostedForPeriod.isGreaterThanOrEqualTo(Money.zero(savingsAccountData.currency()))) { + newPostingTransaction = SavingsAccountTransactionData.interestPosting(savingsAccountData, + interestPostingTransactionDate, interestEarnedToBePostedForPeriod, interestPostingPeriod.isUserPosting()); + } else { + newPostingTransaction = SavingsAccountTransactionData.interestPosting(savingsAccountData, + interestPostingTransactionDate, interestEarnedToBePostedForPeriod.negated(), + interestPostingPeriod.isUserPosting()); + } + + savingsAccountData.updateTransactions(newPostingTransaction); + + if (applyWithHoldTax) { + createWithHoldTransaction(interestEarnedToBePostedForPeriod.getAmount(), interestPostingTransactionDate, + savingsAccountData); + } + 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(); + + final SavingsAccountTransactionData withholdTransaction = findTransactionFor(interestPostingTransactionDate, + withholdTransactions); + + if (withholdTransaction != null) { + withholdTransaction.reverse(); + applyWithHoldTaxForOldTransaction = true; + } + SavingsAccountTransactionData newPostingTransaction; + if (interestEarnedToBePostedForPeriod.isGreaterThanOrEqualTo(Money.zero(savingsAccountData.currency()))) { + newPostingTransaction = SavingsAccountTransactionData.interestPosting(savingsAccountData, + interestPostingTransactionDate, interestEarnedToBePostedForPeriod, + interestPostingPeriod.isUserPosting()); + } else { + newPostingTransaction = SavingsAccountTransactionData.overdraftInterest(savingsAccountData, + interestPostingTransactionDate, interestEarnedToBePostedForPeriod.negated(), + interestPostingPeriod.isUserPosting()); + } + + savingsAccountData.updateTransactions(newPostingTransaction); + + if (applyWithHoldTaxForOldTransaction) { + createWithHoldTransaction(interestEarnedToBePostedForPeriod.getAmount(), interestPostingTransactionDate, + savingsAccountData); + } + recalucateDailyBalanceDetails = true; + } + } + } + } + + if (recalucateDailyBalanceDetails) { + // no openingBalance concept supported yet but probably will to + // allow + // for migrations. + Money openingAccountBalance = Money.zero(savingsAccountData.currency()); + + if (backdatedTxnsAllowedTill) { + if (savingsAccountData.getSummary().getLastInterestCalculationDate() == null) { + openingAccountBalance = Money.zero(savingsAccountData.currency()); + } else { + openingAccountBalance = Money.of(savingsAccountData.currency(), + savingsAccountData.getSummary().getRunningBalanceOnPivotDate()); + } + } + + // update existing transactions so derived balance fields are + // correct. + recalculateDailyBalances(openingAccountBalance, interestPostingUpToDate, backdatedTxnsAllowedTill, savingsAccountData); + } + + if (!backdatedTxnsAllowedTill) { + savingsAccountData.getSummary().updateSummary(savingsAccountData.currency(), + savingsAccountData.getSavingsAccountTransactionSummaryWrapper(), savingsAccountData.getSavingsAccountTransactionData()); + } else { + savingsAccountData.getSummary().updateSummaryWithPivotConfig(savingsAccountData.currency(), + savingsAccountData.getSavingsAccountTransactionSummaryWrapper(), null, + savingsAccountData.getSavingsAccountTransactionData()); + } + + return savingsAccountData; + } + + protected SavingsAccountTransactionData findTransactionFor(final LocalDate postingDate, + final List transactions) { + SavingsAccountTransactionData transaction = null; + for (final SavingsAccountTransactionData savingsAccountTransaction : transactions) { + if (savingsAccountTransaction.occursOn(postingDate)) { + transaction = savingsAccountTransaction; + break; + } + } + return transaction; + } + + public List calculateInterestUsing(final MathContext mc, final LocalDate upToInterestCalculationDate, + boolean isInterestTransfer, final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth, + final LocalDate postInterestOnDate, final boolean backdatedTxnsAllowedTill, final SavingsAccountData savingsAccountData) { + + // no openingBalance concept supported yet but probably will to allow + // for migrations. + Money openingAccountBalance = null; + + // Check global configurations and 'pivot' date is null + if (backdatedTxnsAllowedTill) { + openingAccountBalance = Money.of(savingsAccountData.currency(), savingsAccountData.getSummary().getRunningBalanceOnPivotDate()); + } else { + openingAccountBalance = Money.zero(savingsAccountData.currency()); + } + + // update existing transactions so derived balance fields are + // correct. + recalculateDailyBalances(openingAccountBalance, upToInterestCalculationDate, backdatedTxnsAllowedTill, savingsAccountData); + + // 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(savingsAccountData.getInterestPostingPeriodType()); + + final SavingsCompoundingInterestPeriodType compoundingPeriodType = SavingsCompoundingInterestPeriodType + .fromInt(savingsAccountData.getInterestCompoundingPeriodType()); + + final SavingsInterestCalculationDaysInYearType daysInYearType = SavingsInterestCalculationDaysInYearType + .fromInt(savingsAccountData.getInterestCalculationDaysInYearType()); + + List postedAsOnDates = getManualPostingDates(savingsAccountData); + if (postInterestOnDate != null) { + postedAsOnDates.add(postInterestOnDate); + } + final List postingPeriodIntervals = this.savingsHelper.determineInterestPostingPeriods( + savingsAccountData.getStartInterestCalculationDate(), upToInterestCalculationDate, postingPeriodType, + financialYearBeginningMonth, postedAsOnDates); + + final List allPostingPeriods = new ArrayList<>(); + + Money periodStartingBalance; + if (savingsAccountData.getStartInterestCalculationDate() != null) { + final SavingsAccountTransactionData transaction = retrieveLastTransactions(savingsAccountData); + + if (transaction == null) { + final String defaultUserMessage = "No transactions were found on the specified date " + + savingsAccountData.getStartInterestCalculationDate().toString() + " for account number " + + savingsAccountData.getAccountNo() + " and resource id " + savingsAccountData.getId(); + + final ApiParameterError error = ApiParameterError.parameterError( + "error.msg.savingsaccount.transaction.incorrect.start.interest.calculation.date", defaultUserMessage, + "transactionDate", savingsAccountData.getStartInterestCalculationDate().toString()); + + final List dataValidationErrors = new ArrayList<>(); + dataValidationErrors.add(error); + + throw new PlatformApiDataValidationException(dataValidationErrors); + } + + periodStartingBalance = transaction.getRunningBalance(savingsAccountData.currency()); + } else { + periodStartingBalance = Money.zero(savingsAccountData.currency()); + } + + final SavingsInterestCalculationType interestCalculationType = SavingsInterestCalculationType + .fromInt(savingsAccountData.getInterestCalculationType()); + final BigDecimal interestRateAsFraction = getEffectiveInterestRateAsFraction(mc, upToInterestCalculationDate, savingsAccountData); + final BigDecimal overdraftInterestRateAsFraction = getEffectiveOverdraftInterestRateAsFraction(mc, savingsAccountData); + final Collection interestPostTransactions = this.savingsHelper.fetchPostInterestTransactionIds(savingsAccountData.getId()); + final Money minBalanceForInterestCalculation = Money.of(savingsAccountData.currency(), + minBalanceForInterestCalculation(savingsAccountData)); + final Money minOverdraftForInterestCalculation = Money.of(savingsAccountData.currency(), + savingsAccountData.getMinOverdraftForInterestCalculation()); + final MonetaryCurrency monetaryCurrency = MonetaryCurrency.fromCurrencyData(savingsAccountData.currency()); + + for (final LocalDateInterval periodInterval : postingPeriodIntervals) { + + boolean isUserPosting = false; + if (postedAsOnDates.contains(periodInterval.endDate().plusDays(1))) { + isUserPosting = true; + } + final PostingPeriod postingPeriod = PostingPeriod.createFromDTO(periodInterval, periodStartingBalance, + retreiveOrderedNonInterestPostingTransactions(savingsAccountData), monetaryCurrency, compoundingPeriodType, + interestCalculationType, interestRateAsFraction, daysInYearType.getValue(), upToInterestCalculationDate, + interestPostTransactions, isInterestTransfer, minBalanceForInterestCalculation, + isSavingsInterestPostingAtCurrentPeriodEnd, overdraftInterestRateAsFraction, minOverdraftForInterestCalculation, + isUserPosting, financialYearBeginningMonth); + + periodStartingBalance = postingPeriod.closingBalance(); + + allPostingPeriods.add(postingPeriod); + } + + this.savingsHelper.calculateInterestForAllPostingPeriods(monetaryCurrency, allPostingPeriods, + getLockedInUntilLocalDate(savingsAccountData), false); + + savingsAccountData.getSummary().updateFromInterestPeriodSummaries(monetaryCurrency, allPostingPeriods); + + if (backdatedTxnsAllowedTill) { + savingsAccountData.getSummary().updateSummaryWithPivotConfig(savingsAccountData.currency(), + savingsAccountData.getSavingsAccountTransactionSummaryWrapper(), null, savingsAccountData.getTransactions()); + } else { + savingsAccountData.getSummary().updateSummary(savingsAccountData.currency(), + savingsAccountData.getSavingsAccountTransactionSummaryWrapper(), savingsAccountData.getTransactions()); + } + + return allPostingPeriods; + } + + private List retreiveOrderedNonInterestPostingTransactions(final SavingsAccountData savingsAccountData) { + final List listOfTransactionsSorted = retreiveListOfTransactions(savingsAccountData); + + final List orderedNonInterestPostingTransactions = new ArrayList<>(); + + for (final SavingsAccountTransactionData transaction : listOfTransactionsSorted) { + if (!(transaction.isInterestPostingAndNotReversed() || transaction.isOverdraftInterestAndNotReversed()) + && transaction.isNotReversed()) { + orderedNonInterestPostingTransactions.add(transaction); + } + } + orderedNonInterestPostingTransactions.sort(new SavingsAccountTransactionDataComparator()); + return orderedNonInterestPostingTransactions; + } + + private List retreiveListOfTransactions(final SavingsAccountData savingsAccountData) { + if (savingsAccountData.getTransactions() != null && savingsAccountData.getTransactions().size() == 1) { + return savingsAccountData.getTransactions(); + } + + final List listOfTransactionsSorted = new ArrayList<>(); + listOfTransactionsSorted.addAll(savingsAccountData.getTransactions()); + + final SavingsAccountTransactionDataComparator transactionComparator = new SavingsAccountTransactionDataComparator(); + Collections.sort(listOfTransactionsSorted, transactionComparator); + return listOfTransactionsSorted; + } + + protected LocalDate getLockedInUntilLocalDate(final SavingsAccountData savingsAccount) { + LocalDate lockedInUntilLocalDate = null; + if (savingsAccount.getLockedInUntilDate() != null) { + lockedInUntilLocalDate = savingsAccount.getActivationLocalDate(); + // lockedInUntilLocalDate = LocalDate.ofInstant(this.lockedInUntilDate.toInstant(), + // DateUtils.getDateTimeZoneOfTenant()); + } + return lockedInUntilLocalDate; + } + + private BigDecimal minBalanceForInterestCalculation(final SavingsAccountData savingsAccountData) { + return savingsAccountData.getMinBalanceForInterestCalculation(); + } + + private BigDecimal getEffectiveOverdraftInterestRateAsFraction(MathContext mc, final SavingsAccountData savingsAccountData) { + return savingsAccountData.getNominalAnnualInterestRateOverdraft().divide(BigDecimal.valueOf(100L), mc); + } + + @SuppressWarnings("unused") + private BigDecimal getEffectiveInterestRateAsFraction(final MathContext mc, final LocalDate upToInterestCalculationDate, + final SavingsAccountData savingsAccountData) { + return savingsAccountData.getNominalAnnualInterestRate().divide(BigDecimal.valueOf(100L), mc); + } + + public List getTransactions(final SavingsAccountData savingsAccountData) { + return savingsAccountData.getTransactions(); + } + + private SavingsAccountTransactionData retrieveLastTransactions(final SavingsAccountData savingsAccountData) { + if (savingsAccountData.getTransactions() != null && savingsAccountData.getTransactions().size() == 1) { + return savingsAccountData.getTransactions().get(0); + } + final List listOfTransactionsSorted = new ArrayList<>(); + listOfTransactionsSorted.addAll(savingsAccountData.getTransactions()); + final SavingsAccountTransactionDataComparator transactionComparator = new SavingsAccountTransactionDataComparator(); + Collections.sort(listOfTransactionsSorted, transactionComparator); + return listOfTransactionsSorted.get(0); + } + + public LocalDate getStartInterestCalculationDate(final SavingsAccountData savingsAccountData) { + LocalDate startInterestCalculationLocalDate = null; + if (savingsAccountData.getStartInterestCalculationDate() != null) { + startInterestCalculationLocalDate = savingsAccountData.getStartInterestCalculationDate(); + } else { + startInterestCalculationLocalDate = getActivationLocalDate(savingsAccountData); + } + return startInterestCalculationLocalDate; + } + + public LocalDate getActivationLocalDate(final SavingsAccountData savingsAccountData) { + LocalDate activationLocalDate = null; + if (savingsAccountData.getActivationLocalDate() != null) { + activationLocalDate = savingsAccountData.getActivationLocalDate(); + } + return activationLocalDate; + } + + public List getManualPostingDates(final SavingsAccountData savingsAccountData) { + List transactions = new ArrayList<>(); + for (SavingsAccountTransactionData trans : savingsAccountData.getSavingsAccountTransactionData()) { + if (trans.isInterestPosting() && trans.isNotReversed() && trans.isManualTransaction()) { + transactions.add(trans.getTransactionLocalDate()); + } + } + return transactions; + } + + protected void recalculateDailyBalances(final Money openingAccountBalance, final LocalDate interestPostingUpToDate, + final boolean backdatedTxnsAllowedTill, final SavingsAccountData savingsAccountData) { + + Money runningBalance = openingAccountBalance.copy(); + + List accountTransactionsSorted = retreiveListOfTransactions(savingsAccountData); + + boolean isTransactionsModified = false; + + for (final SavingsAccountTransactionData transaction : accountTransactionsSorted) { + if (transaction.isReversed()) { + transaction.zeroBalanceFields(); + } else { + Money overdraftAmount = Money.zero(savingsAccountData.currency()); + Money transactionAmount = Money.zero(savingsAccountData.currency()); + if (transaction.isCredit() || transaction.isAmountRelease()) { + if (runningBalance.isLessThanZero()) { + Money diffAmount = Money.of(savingsAccountData.currency(), transaction.getAmount()).plus(runningBalance); + if (diffAmount.isGreaterThanZero()) { + overdraftAmount = Money.of(savingsAccountData.currency(), transaction.getAmount()).minus(diffAmount); + } else { + overdraftAmount = Money.of(savingsAccountData.currency(), transaction.getAmount()); + } + } + transactionAmount = transactionAmount.plus(transaction.getAmount()); + } else if (transaction.isDebit() || transaction.isAmountOnHold()) { + if (runningBalance.isLessThanZero()) { + overdraftAmount = Money.of(savingsAccountData.currency(), transaction.getAmount()); + } + transactionAmount = transactionAmount.minus(transaction.getAmount()); + } + + runningBalance = runningBalance.plus(transactionAmount); + transaction.updateRunningBalance(runningBalance); + if (overdraftAmount.isZero() && runningBalance.isLessThanZero()) { + overdraftAmount = overdraftAmount.plus(runningBalance.getAmount().negate()); + } + if (transaction.getId() == null && overdraftAmount.isGreaterThanZero()) { + transaction.updateOverdraftAmount(overdraftAmount.getAmount()); + } else if (overdraftAmount.isNotEqualTo(Money.of(savingsAccountData.currency(), transaction.getOverdraftAmount()))) { + SavingsAccountTransactionData accountTransaction = SavingsAccountTransactionData.copyTransaction(transaction); + if (transaction.isChargeTransaction()) { + Set chargesPaidBy = transaction.getSavingsAccountChargesPaid(); + final Set newChargePaidBy = new HashSet<>(); + chargesPaidBy.forEach( + x -> newChargePaidBy.add(SavingsAccountChargesPaidByData.instance(x.getChargeId(), x.getAmount()))); + accountTransaction.getSavingsAccountChargesPaid().addAll(newChargePaidBy); + } + transaction.reverse(); + if (overdraftAmount.isGreaterThanZero()) { + accountTransaction.updateOverdraftAmount(overdraftAmount.getAmount()); + } + accountTransaction.updateRunningBalance(runningBalance); + addTransactionToExisting(accountTransaction, savingsAccountData); + + isTransactionsModified = true; + } + + } + } + + if (isTransactionsModified) { + accountTransactionsSorted = retreiveListOfTransactions(savingsAccountData); + } + resetAccountTransactionsEndOfDayBalances(accountTransactionsSorted, interestPostingUpToDate, savingsAccountData); + } + + public void addTransactionToExisting(final SavingsAccountTransactionData transaction, final SavingsAccountData savingsAccountData) { + savingsAccountData.updateTransactions(transaction); + } + + private List findWithHoldSavingsTransactionsWithPivotConfig( + final SavingsAccountData savingsAccountData) { + final List withholdTransactions = new ArrayList<>(); + List trans = savingsAccountData.getSavingsAccountTransactionData(); + for (final SavingsAccountTransactionData transaction : trans) { + if (transaction.isWithHoldTaxAndNotReversed()) { + withholdTransactions.add(transaction); + } + } + return withholdTransactions; + } + + private boolean createWithHoldTransaction(final BigDecimal amount, final LocalDate date, final SavingsAccountData savingsAccountData) { + boolean isTaxAdded = false; + if (savingsAccountData.getTaxGroupData() != null && amount.compareTo(BigDecimal.ZERO) > 0) { + Map taxSplit = TaxUtils.splitTaxData(amount, date, + savingsAccountData.getTaxGroupData().getTaxAssociations().stream().collect(Collectors.toSet()), amount.scale()); + BigDecimal totalTax = TaxUtils.totalTaxDataAmount(taxSplit); + if (totalTax.compareTo(BigDecimal.ZERO) > 0) { + SavingsAccountTransactionData withholdTransaction = SavingsAccountTransactionData.withHoldTax(savingsAccountData, date, + Money.of(savingsAccountData.currency(), totalTax), taxSplit); + + savingsAccountData.getSavingsAccountTransactionData().add(withholdTransaction); + // if (backdatedTxnsAllowedTill) { + // addTransactionToExisting(withholdTransaction); + // } else { + // addTransaction(withholdTransaction); + // } + isTaxAdded = true; + } + } + return isTaxAdded; + } + + protected SavingsAccountTransactionData findInterestPostingTransactionFor(final LocalDate postingDate, + final SavingsAccountData savingsAccountData) { + SavingsAccountTransactionData postingTransation = null; + List trans = savingsAccountData.getSavingsAccountTransactionData(); + for (final SavingsAccountTransactionData transaction : trans) { + if ((transaction.isInterestPostingAndNotReversed() || transaction.isOverdraftInterestAndNotReversed()) + && transaction.occursOn(postingDate)) { + postingTransation = transaction; + break; + } + } + return postingTransation; + } + + protected void resetAccountTransactionsEndOfDayBalances(final List accountTransactionsSorted, + final LocalDate interestPostingUpToDate, final SavingsAccountData savingsAccountData) { + // loop over transactions in reverse + LocalDate endOfBalanceDate = interestPostingUpToDate; + for (int i = accountTransactionsSorted.size() - 1; i >= 0; i--) { + final SavingsAccountTransactionData transaction = accountTransactionsSorted.get(i); + if (transaction.isNotReversed() + && !(transaction.isInterestPostingAndNotReversed() || transaction.isOverdraftInterestAndNotReversed())) { + transaction.updateCumulativeBalanceAndDates(MonetaryCurrency.fromCurrencyData(savingsAccountData.currency()), + endOfBalanceDate); + // this transactions transaction date is end of balance date for + // previous transaction. + endOfBalanceDate = transaction.getTransactionLocalDate().minusDays(1); + } + } + } + + private boolean isWithHoldTaxApplicableForInterestPosting(final SavingsAccountData savingsAccountData) { + return this.withHoldTax(savingsAccountData) && this.depositAccountType(savingsAccountData).isSavingsDeposit(); + } + + private boolean withHoldTax(final SavingsAccountData savingsAccountData) { + return savingsAccountData.withHoldTax(); + } + + public DepositAccountType depositAccountType(final SavingsAccountData savingsAccountData) { + return savingsAccountData.depositAccountType(); + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformService.java index 9c794fb2f..b425da10d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformService.java @@ -63,4 +63,9 @@ public interface SavingsAccountReadPlatformService { String retrieveAccountNumberByAccountId(Long accountId); List getAccountsIdsByStatusPaged(Integer status, int pageSize, Long maxSavingsIdInList); + + List retrieveAllSavingsDataForInterestPosting(boolean backdatedTxnsAllowedTill, int pageSize, Integer status, + Long maxSavingsId); + + List retrieveAllTransactionData(List refNo); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java index 8c2923dee..26e041de1 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java @@ -27,8 +27,12 @@ import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; import java.util.List; import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.accounting.common.AccountingRuleType; +import org.apache.fineract.accounting.glaccount.data.GLAccountData; import org.apache.fineract.infrastructure.core.data.EnumOptionData; import org.apache.fineract.infrastructure.core.domain.JdbcSupport; import org.apache.fineract.infrastructure.core.service.DateUtils; @@ -69,15 +73,20 @@ import org.apache.fineract.portfolio.savings.data.SavingsAccountSummaryData; import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData; import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionEnumData; import org.apache.fineract.portfolio.savings.data.SavingsProductData; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountChargesPaidByData; import org.apache.fineract.portfolio.savings.domain.SavingsAccountStatusType; import org.apache.fineract.portfolio.savings.domain.SavingsAccountSubStatusEnum; import org.apache.fineract.portfolio.savings.exception.SavingsAccountNotFoundException; +import org.apache.fineract.portfolio.tax.data.TaxComponentData; +import org.apache.fineract.portfolio.tax.data.TaxDetailsData; import org.apache.fineract.portfolio.tax.data.TaxGroupData; import org.apache.fineract.useradministration.domain.AppUser; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.ResultSetExtractor; import org.springframework.jdbc.core.RowMapper; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; @@ -98,7 +107,9 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead // mappers private final SavingsAccountTransactionTemplateMapper transactionTemplateMapper; private final SavingsAccountTransactionsMapper transactionsMapper; + private final SavingsAccountTransactionsForBatchMapper savingsAccountTransactionsForBatchMapper; private final SavingAccountMapper savingAccountMapper; + private final SavingAccountMapperForInterestPosting savingAccountMapperForInterestPosting; // private final SavingsAccountAnnualFeeMapper annualFeeMapper; // pagination @@ -106,6 +117,7 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead private final EntityDatatableChecksReadService entityDatatableChecksReadService; private final ColumnValidator columnValidator; + private final SavingsAccountAssembler savingAccountAssembler; @Autowired public SavingsAccountReadPlatformServiceImpl(final PlatformSecurityContext context, final RoutingDataSource dataSource, @@ -113,7 +125,8 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead final SavingsProductReadPlatformService savingProductReadPlatformService, final StaffReadPlatformService staffReadPlatformService, final SavingsDropdownReadPlatformService dropdownReadPlatformService, final ChargeReadPlatformService chargeReadPlatformService, - final EntityDatatableChecksReadService entityDatatableChecksReadService, final ColumnValidator columnValidator) { + final EntityDatatableChecksReadService entityDatatableChecksReadService, final ColumnValidator columnValidator, + final SavingsAccountAssembler savingAccountAssembler) { this.context = context; this.jdbcTemplate = new JdbcTemplate(dataSource); this.clientReadPlatformService = clientReadPlatformService; @@ -123,11 +136,14 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead this.dropdownReadPlatformService = dropdownReadPlatformService; this.transactionTemplateMapper = new SavingsAccountTransactionTemplateMapper(); this.transactionsMapper = new SavingsAccountTransactionsMapper(); + this.savingsAccountTransactionsForBatchMapper = new SavingsAccountTransactionsForBatchMapper(); this.savingAccountMapper = new SavingAccountMapper(); // this.annualFeeMapper = new SavingsAccountAnnualFeeMapper(); this.chargeReadPlatformService = chargeReadPlatformService; this.entityDatatableChecksReadService = entityDatatableChecksReadService; this.columnValidator = columnValidator; + this.savingAccountMapperForInterestPosting = new SavingAccountMapperForInterestPosting(); + this.savingAccountAssembler = savingAccountAssembler; } @Override @@ -230,6 +246,495 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead } } + @Override + public List retrieveAllTransactionData(final List refNo) throws DataAccessException { + String inSql = String.join(",", Collections.nCopies(refNo.size(), "?")); + String sql = "select " + this.savingsAccountTransactionsForBatchMapper.schema() + " where tr.ref_no in (%s)"; + Object[] params = new Object[refNo.size()]; + int i = 0; + for (String element : refNo) { + params[i] = element; + i++; + } + return this.jdbcTemplate.query(String.format(sql, inSql), this.savingsAccountTransactionsForBatchMapper, params); + } + + @Override + public List retrieveAllSavingsDataForInterestPosting(final boolean backdatedTxnsAllowedTill, final int pageSize, + final Integer status, final Long maxSavingsId) { + LocalDate currentDate = DateUtils.getLocalDateOfTenant().minusDays(1); + + String sql = "select " + this.savingAccountMapperForInterestPosting.schema() + + "join (select a.id from m_savings_account a where a.id > ? and a.status_enum = ? limit ?) b on b.id = sa.id "; + if (backdatedTxnsAllowedTill) { + sql = sql + + "where if (sa.interest_posted_till_date is not null, tr.transaction_date >= sa.interest_posted_till_date, tr.transaction_date >= sa.activatedon_date) "; + } + + sql = sql + "and apm.product_type=2 and sa.interest_posted_till_date<" + java.sql.Date.valueOf(currentDate); + sql = sql + " order by sa.id, tr.transaction_date, tr.created_date, tr.id"; + + List savingsAccountDataList = this.jdbcTemplate.query(sql, this.savingAccountMapperForInterestPosting, + new Object[] { maxSavingsId, status, pageSize }); + for (SavingsAccountData savingsAccountData : savingsAccountDataList) { + this.savingAccountAssembler.assembleSavings(savingsAccountData); + } + return savingsAccountDataList; + } + + private static final class SavingAccountMapperForInterestPosting implements ResultSetExtractor> { + + private final String schemaSql; + + SavingAccountMapperForInterestPosting() { + final StringBuilder sqlBuilder = new StringBuilder(400); + sqlBuilder.append("sa.id as id, sa.account_no as accountNo, sa.external_id as externalId, "); + sqlBuilder.append("sa.deposit_type_enum as depositType, "); + sqlBuilder.append("c.id as clientId, c.office_id as clientOfficeId, "); + sqlBuilder.append("g.id as groupId, g.office_id as groupOfficeId, "); + sqlBuilder.append("sa.status_enum as statusEnum, "); + sqlBuilder.append("sa.sub_status_enum as subStatusEnum, "); + sqlBuilder.append("sa.submittedon_date as submittedOnDate,"); + + sqlBuilder.append("sa.rejectedon_date as rejectedOnDate,"); + + sqlBuilder.append("sa.withdrawnon_date as withdrawnOnDate,"); + + sqlBuilder.append("sa.approvedon_date as approvedOnDate,"); + + sqlBuilder.append("sa.activatedon_date as activatedOnDate,"); + + sqlBuilder.append("sa.closedon_date as closedOnDate,"); + + sqlBuilder.append( + "sa.currency_code as currencyCode, sa.currency_digits as currencyDigits, sa.currency_multiplesof as inMultiplesOf, "); + + sqlBuilder.append("sa.nominal_annual_interest_rate as nominalAnnualInterestRate, "); + sqlBuilder.append("sa.interest_compounding_period_enum as interestCompoundingPeriodType, "); + sqlBuilder.append("sa.interest_posting_period_enum as interestPostingPeriodType, "); + sqlBuilder.append("sa.interest_calculation_type_enum as interestCalculationType, "); + sqlBuilder.append("sa.interest_calculation_days_in_year_type_enum as interestCalculationDaysInYearType, "); + sqlBuilder.append("sa.min_required_opening_balance as minRequiredOpeningBalance, "); + sqlBuilder.append("sa.lockin_period_frequency as lockinPeriodFrequency,"); + sqlBuilder.append("sa.lockin_period_frequency_enum as lockinPeriodFrequencyType, "); + sqlBuilder.append("sa.withdrawal_fee_for_transfer as withdrawalFeeForTransfers, "); + sqlBuilder.append("sa.allow_overdraft as allowOverdraft, "); + sqlBuilder.append("sa.overdraft_limit as overdraftLimit, "); + sqlBuilder.append("sa.nominal_annual_interest_rate_overdraft as nominalAnnualInterestRateOverdraft, "); + sqlBuilder.append("sa.min_overdraft_for_interest_calculation as minOverdraftForInterestCalculation, "); + sqlBuilder.append("sa.total_deposits_derived as totalDeposits, "); + sqlBuilder.append("sa.total_withdrawals_derived as totalWithdrawals, "); + sqlBuilder.append("sa.total_withdrawal_fees_derived as totalWithdrawalFees, "); + sqlBuilder.append("sa.total_annual_fees_derived as totalAnnualFees, "); + sqlBuilder.append("sa.total_interest_earned_derived as totalInterestEarned, "); + sqlBuilder.append("sa.total_interest_posted_derived as totalInterestPosted, "); + sqlBuilder.append("sa.total_overdraft_interest_derived as totalOverdraftInterestDerived, "); + sqlBuilder.append("sa.account_balance_derived as accountBalance, "); + sqlBuilder.append("sa.total_fees_charge_derived as totalFeeCharge, "); + sqlBuilder.append("sa.total_penalty_charge_derived as totalPenaltyCharge, "); + sqlBuilder.append("sa.min_balance_for_interest_calculation as minBalanceForInterestCalculation,"); + sqlBuilder.append("sa.min_required_balance as minRequiredBalance, "); + sqlBuilder.append("sa.enforce_min_required_balance as enforceMinRequiredBalance, "); + sqlBuilder.append("sa.on_hold_funds_derived as onHoldFunds, "); + sqlBuilder.append("sa.withhold_tax as withHoldTax, "); + sqlBuilder.append("sa.total_withhold_tax_derived as totalWithholdTax, "); + sqlBuilder.append("sa.last_interest_calculation_date as lastInterestCalculationDate, "); + sqlBuilder.append("sa.total_savings_amount_on_hold as onHoldAmount, "); + sqlBuilder.append("sa.interest_posted_till_date as interestPostedTillDate, "); + sqlBuilder.append("tg.id as taxGroupId, "); + sqlBuilder.append("(select IFNULL(max(sat.transaction_date),sa.activatedon_date) "); + sqlBuilder.append("from m_savings_account_transaction as sat "); + sqlBuilder.append("where sat.is_reversed = 0 "); + sqlBuilder.append("and sat.transaction_type_enum in (1,2) "); + sqlBuilder.append("and sat.savings_account_id = sa.id) as lastActiveTransactionDate, "); + sqlBuilder.append("sp.id as productId, "); + sqlBuilder.append("sp.is_dormancy_tracking_active as isDormancyTrackingActive, "); + sqlBuilder.append("sp.days_to_inactive as daysToInactive, "); + sqlBuilder.append("sp.days_to_dormancy as daysToDormancy, "); + sqlBuilder.append("sp.days_to_escheat as daysToEscheat, "); + sqlBuilder.append("sp.accounting_type as accountingType, "); + sqlBuilder.append("tr.id as transactionId, tr.transaction_type_enum as transactionType, "); + sqlBuilder.append("tr.transaction_date as transactionDate, tr.amount as transactionAmount,"); + sqlBuilder.append("tr.created_date as createdDate,tr.cumulative_balance_derived as cumulativeBalance,"); + sqlBuilder.append("tr.running_balance_derived as runningBalance, tr.is_reversed as reversed,"); + sqlBuilder.append("tr.balance_end_date_derived as balanceEndDate, tr.overdraft_amount_derived as overdraftAmount,"); + sqlBuilder.append("tr.is_manual as manualTransaction,tr.office_id as officeId, "); + sqlBuilder.append("pd.payment_type_id as paymentType,pd.account_number as accountNumber,pd.check_number as checkNumber, "); + sqlBuilder.append("pd.receipt_number as receiptNumber, pd.bank_number as bankNumber,pd.routing_code as routingCode, "); + sqlBuilder.append("pt.value as paymentTypeName, "); + sqlBuilder.append("msacpb.amount as paidByAmount, msacpb.id as chargesPaidById, "); + sqlBuilder.append( + "msac.id as chargeId, msac.amount as chargeAmount, msac.charge_time_enum as chargeTimeType, msac.is_penalty as isPenaltyCharge, "); + sqlBuilder.append("txd.id as taxDetailsId, txd.amount as taxAmount, "); + sqlBuilder.append("apm.gl_account_id as glAccountIdForInterestOnSavings, apm1.gl_account_id as glAccountIdForSavingsControl, "); + sqlBuilder.append( + "mtc.id as taxComponentId, mtc.debit_account_id as debitAccountId, mtc.credit_account_id as creditAccountId, mtc.percentage as taxPercentage "); + sqlBuilder.append("from m_savings_account sa "); + sqlBuilder.append("join m_savings_product sp ON sa.product_id = sp.id "); + sqlBuilder.append("join m_currency curr on curr.code = sa.currency_code "); + sqlBuilder.append("join m_savings_account_transaction tr on sa.id = tr.savings_account_id "); + sqlBuilder.append("left join m_payment_detail pd on pd.id = tr.payment_detail_id "); + sqlBuilder.append("left join m_payment_type pt on pt.id = pd.payment_type_id "); + sqlBuilder.append("left join m_savings_account_charge_paid_by msacpb on msacpb.savings_account_transaction_id = tr.id "); + sqlBuilder.append("left join m_savings_account_charge msac on msac.id = msacpb.savings_account_charge_id "); + sqlBuilder.append("left join m_client c ON c.id = sa.client_id "); + sqlBuilder.append("left join m_group g ON g.id = sa.group_id "); + sqlBuilder.append("left join m_tax_group tg on tg.id = sa.tax_group_id "); + sqlBuilder.append("left join m_savings_account_transaction_tax_details txd on txd.savings_transaction_id = tr.id "); + sqlBuilder.append("left join m_tax_component mtc on mtc.id = txd.tax_component_id "); + sqlBuilder.append("left join acc_product_mapping apm on apm.product_id = sp.id and apm.financial_account_type=3 "); + sqlBuilder.append("left join acc_product_mapping apm1 on apm1.product_id = sp.id and apm1.financial_account_type=2 "); + + this.schemaSql = sqlBuilder.toString(); + } + + public String schema() { + return this.schemaSql; + } + + @Override + public List extractData(final ResultSet rs) throws SQLException { + + List savingsAccountDataList = new ArrayList<>(); + HashMap savingsMap = new HashMap<>(); + String currencyCode = null; + Integer currencyDigits = null; + Integer inMultiplesOf = null; + CurrencyData currency = null; + HashMap transMap = new HashMap<>(); + HashMap taxDetails = new HashMap<>(); + HashMap chargeDetails = new HashMap<>(); + SavingsAccountTransactionData savingsAccountTransactionData = null; + SavingsAccountData savingsAccountData = null; + int count = 0; + + while (rs.next()) { + final Long id = rs.getLong("id"); + final Long transactionId = rs.getLong("transactionId"); + final Long taxDetailId = JdbcSupport.getLongDefaultToNullIfZero(rs, "taxDetailsId"); + final Long taxComponentId = JdbcSupport.getLongDefaultToNullIfZero(rs, "taxComponentId"); + final String accountNo = rs.getString("accountNo"); + final Long chargeId = rs.getLong("chargeId"); + + if (!savingsMap.containsValue(id)) { + if (count > 0) { + savingsAccountDataList.add(savingsAccountData); + } + count++; + savingsMap.put("id", id); + + final String externalId = rs.getString("externalId"); + final Integer depositTypeId = rs.getInt("depositType"); + final EnumOptionData depositType = SavingsEnumerations.depositType(depositTypeId); + final Long groupId = JdbcSupport.getLong(rs, "groupId"); + final Long groupOfficeId = JdbcSupport.getLong(rs, "groupOfficeId"); + final GroupGeneralData groupGeneralData = new GroupGeneralData(groupId, groupOfficeId); + + final Long clientId = JdbcSupport.getLong(rs, "clientId"); + final Long clientOfficeId = JdbcSupport.getLong(rs, "clientOfficeId"); + final ClientData clientData = ClientData.createClientForInterestPosting(clientId, clientOfficeId); + + final Long glAccountIdForInterestOnSavings = rs.getLong("glAccountIdForInterestOnSavings"); + final Long glAccountIdForSavingsControl = rs.getLong("glAccountIdForSavingsControl"); + + final Long productId = rs.getLong("productId"); + final Integer accountType = rs.getInt("accountingType"); + final AccountingRuleType accountingRuleType = AccountingRuleType.fromInt(accountType); + final EnumOptionData enumOptionDataForAccounting = new EnumOptionData(accountType.longValue(), + accountingRuleType.getCode(), accountingRuleType.getValue().toString()); + final SavingsProductData savingsProductData = SavingsProductData.createForInterestPosting(productId, + enumOptionDataForAccounting); + + final Integer statusEnum = JdbcSupport.getInteger(rs, "statusEnum"); + final SavingsAccountStatusEnumData status = SavingsEnumerations.status(statusEnum); + final Integer subStatusEnum = JdbcSupport.getInteger(rs, "subStatusEnum"); + final SavingsAccountSubStatusEnumData subStatus = SavingsEnumerations.subStatus(subStatusEnum); + final LocalDate lastActiveTransactionDate = JdbcSupport.getLocalDate(rs, "lastActiveTransactionDate"); + final boolean isDormancyTrackingActive = rs.getBoolean("isDormancyTrackingActive"); + final Integer numDaysToInactive = JdbcSupport.getInteger(rs, "daysToInactive"); + final Integer numDaysToDormancy = JdbcSupport.getInteger(rs, "daysToDormancy"); + final Integer numDaysToEscheat = JdbcSupport.getInteger(rs, "daysToEscheat"); + Integer daysToInactive = null; + Integer daysToDormancy = null; + Integer daysToEscheat = null; + + LocalDate localTenantDate = DateUtils.getLocalDateOfTenant(); + if (isDormancyTrackingActive && statusEnum.equals(SavingsAccountStatusType.ACTIVE.getValue())) { + if (subStatusEnum < SavingsAccountSubStatusEnum.ESCHEAT.getValue()) { + daysToEscheat = Math.toIntExact( + ChronoUnit.DAYS.between(localTenantDate, lastActiveTransactionDate.plusDays(numDaysToEscheat))); + } + if (subStatusEnum < SavingsAccountSubStatusEnum.DORMANT.getValue()) { + daysToDormancy = Math.toIntExact( + ChronoUnit.DAYS.between(localTenantDate, lastActiveTransactionDate.plusDays(numDaysToDormancy))); + } + if (subStatusEnum < SavingsAccountSubStatusEnum.INACTIVE.getValue()) { + daysToInactive = Math.toIntExact( + ChronoUnit.DAYS.between(localTenantDate, lastActiveTransactionDate.plusDays(numDaysToInactive))); + } + } + final LocalDate approvedOnDate = JdbcSupport.getLocalDate(rs, "approvedOnDate"); + final LocalDate withdrawnOnDate = JdbcSupport.getLocalDate(rs, "withdrawnOnDate"); + final LocalDate submittedOnDate = JdbcSupport.getLocalDate(rs, "submittedOnDate"); + final LocalDate activatedOnDate = JdbcSupport.getLocalDate(rs, "activatedOnDate"); + final LocalDate closedOnDate = JdbcSupport.getLocalDate(rs, "closedOnDate"); + final SavingsAccountApplicationTimelineData timeline = new SavingsAccountApplicationTimelineData(submittedOnDate, null, + null, null, null, null, null, null, withdrawnOnDate, null, null, null, approvedOnDate, null, null, null, + activatedOnDate, null, null, null, closedOnDate, null, null, null); + + currencyCode = rs.getString("currencyCode"); + currencyDigits = JdbcSupport.getInteger(rs, "currencyDigits"); + inMultiplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf"); + currency = new CurrencyData(currencyCode, currencyDigits, inMultiplesOf); + + final BigDecimal totalDeposits = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalDeposits"); + final BigDecimal totalWithdrawals = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalWithdrawals"); + final BigDecimal totalWithdrawalFees = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalWithdrawalFees"); + final BigDecimal totalAnnualFees = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalAnnualFees"); + + final BigDecimal totalInterestEarned = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalInterestEarned"); + final BigDecimal totalInterestPosted = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "totalInterestPosted"); + final BigDecimal accountBalance = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "accountBalance"); + final BigDecimal totalFeeCharge = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalFeeCharge"); + final BigDecimal totalPenaltyCharge = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalPenaltyCharge"); + final BigDecimal totalOverdraftInterestDerived = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, + "totalOverdraftInterestDerived"); + final BigDecimal totalWithholdTax = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalWithholdTax"); + final LocalDate interestPostedTillDate = JdbcSupport.getLocalDate(rs, "interestPostedTillDate"); + + final BigDecimal minBalanceForInterestCalculation = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, + "minBalanceForInterestCalculation"); + final BigDecimal onHoldFunds = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "onHoldFunds"); + + final BigDecimal onHoldAmount = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "onHoldAmount"); + + BigDecimal availableBalance = accountBalance; + if (availableBalance != null && onHoldFunds != null) { + + availableBalance = availableBalance.subtract(onHoldFunds); + } + + if (availableBalance != null && onHoldAmount != null) { + + availableBalance = availableBalance.subtract(onHoldAmount); + } + + BigDecimal interestNotPosted = BigDecimal.ZERO; + LocalDate lastInterestCalculationDate = null; + if (totalInterestEarned != null) { + interestNotPosted = totalInterestEarned.subtract(totalInterestPosted).add(totalOverdraftInterestDerived); + lastInterestCalculationDate = JdbcSupport.getLocalDate(rs, "lastInterestCalculationDate"); + } + + final SavingsAccountSummaryData summary = new SavingsAccountSummaryData(currency, totalDeposits, totalWithdrawals, + totalWithdrawalFees, totalAnnualFees, totalInterestEarned, totalInterestPosted, accountBalance, totalFeeCharge, + totalPenaltyCharge, totalOverdraftInterestDerived, totalWithholdTax, interestNotPosted, + lastInterestCalculationDate, availableBalance, interestPostedTillDate); + summary.setPrevInterestPostedTillDate(interestPostedTillDate); + + final boolean withHoldTax = rs.getBoolean("withHoldTax"); + final Long taxGroupId = JdbcSupport.getLongDefaultToNullIfZero(rs, "taxGroupId"); + TaxGroupData taxGroupData = null; + if (taxGroupId != null) { + taxGroupData = TaxGroupData.lookup(taxGroupId, null); + } + + final BigDecimal nominalAnnualInterestRate = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, + "nominalAnnualInterestRate"); + + final EnumOptionData interestCompoundingPeriodType = SavingsEnumerations.compoundingInterestPeriodType( + SavingsCompoundingInterestPeriodType.fromInt(JdbcSupport.getInteger(rs, "interestCompoundingPeriodType"))); + + final EnumOptionData interestPostingPeriodType = SavingsEnumerations.interestPostingPeriodType( + SavingsPostingInterestPeriodType.fromInt(JdbcSupport.getInteger(rs, "interestPostingPeriodType"))); + + final EnumOptionData interestCalculationType = SavingsEnumerations.interestCalculationType( + SavingsInterestCalculationType.fromInt(JdbcSupport.getInteger(rs, "interestCalculationType"))); + + final EnumOptionData interestCalculationDaysInYearType = SavingsEnumerations + .interestCalculationDaysInYearType(SavingsInterestCalculationDaysInYearType + .fromInt(JdbcSupport.getInteger(rs, "interestCalculationDaysInYearType"))); + + final BigDecimal minRequiredOpeningBalance = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, + "minRequiredOpeningBalance"); + + final Integer lockinPeriodFrequency = JdbcSupport.getInteger(rs, "lockinPeriodFrequency"); + EnumOptionData lockinPeriodFrequencyType = null; + final Integer lockinPeriodFrequencyTypeValue = JdbcSupport.getInteger(rs, "lockinPeriodFrequencyType"); + if (lockinPeriodFrequencyTypeValue != null) { + final SavingsPeriodFrequencyType lockinPeriodType = SavingsPeriodFrequencyType + .fromInt(lockinPeriodFrequencyTypeValue); + lockinPeriodFrequencyType = SavingsEnumerations.lockinPeriodFrequencyType(lockinPeriodType); + } + + final boolean withdrawalFeeForTransfers = rs.getBoolean("withdrawalFeeForTransfers"); + + final boolean allowOverdraft = rs.getBoolean("allowOverdraft"); + final BigDecimal overdraftLimit = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "overdraftLimit"); + final BigDecimal nominalAnnualInterestRateOverdraft = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, + "nominalAnnualInterestRateOverdraft"); + final BigDecimal minOverdraftForInterestCalculation = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, + "minOverdraftForInterestCalculation"); + + final BigDecimal minRequiredBalance = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "minRequiredBalance"); + final boolean enforceMinRequiredBalance = rs.getBoolean("enforceMinRequiredBalance"); + savingsAccountData = SavingsAccountData.instance(id, accountNo, depositType, externalId, null, null, null, null, + productId, null, null, null, status, subStatus, timeline, currency, nominalAnnualInterestRate, + interestCompoundingPeriodType, interestPostingPeriodType, interestCalculationType, + interestCalculationDaysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType, + withdrawalFeeForTransfers, summary, allowOverdraft, overdraftLimit, minRequiredBalance, + enforceMinRequiredBalance, minBalanceForInterestCalculation, onHoldFunds, nominalAnnualInterestRateOverdraft, + minOverdraftForInterestCalculation, withHoldTax, taxGroupData, lastActiveTransactionDate, + isDormancyTrackingActive, daysToInactive, daysToDormancy, daysToEscheat, onHoldAmount); + + savingsAccountData.setClientData(clientData); + savingsAccountData.setGroupGeneralData(groupGeneralData); + savingsAccountData.setSavingsProduct(savingsProductData); + savingsAccountData.setGlAccountIdForInterestOnSavings(glAccountIdForInterestOnSavings); + savingsAccountData.setGlAccountIdForSavingsControl(glAccountIdForSavingsControl); + } + + if (!transMap.containsValue(transactionId)) { + + final int transactionTypeInt = JdbcSupport.getInteger(rs, "transactionType"); + final SavingsAccountTransactionEnumData transactionType = SavingsEnumerations.transactionType(transactionTypeInt); + + final LocalDate date = JdbcSupport.getLocalDate(rs, "transactionDate"); + final LocalDate balanceEndDate = JdbcSupport.getLocalDate(rs, "balanceEndDate"); + final LocalDate transSubmittedOnDate = JdbcSupport.getLocalDate(rs, "createdDate"); + final BigDecimal amount = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "transactionAmount"); + final BigDecimal overdraftAmount = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "overdraftAmount"); + final BigDecimal outstandingChargeAmount = null; + final BigDecimal runningBalance = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "runningBalance"); + final boolean reversed = rs.getBoolean("reversed"); + final boolean isManualTransaction = rs.getBoolean("manualTransaction"); + final Long officeId = rs.getLong("officeId"); + final BigDecimal cumulativeBalance = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "cumulativeBalance"); + + final boolean postInterestAsOn = false; + + PaymentDetailData paymentDetailData = null; + if (transactionType.isDepositOrWithdrawal()) { + final Long paymentTypeId = JdbcSupport.getLong(rs, "paymentType"); + if (paymentTypeId != null) { + final String typeName = rs.getString("paymentTypeName"); + final PaymentTypeData paymentTypeData = new PaymentTypeData(paymentTypeId, typeName, null, false, null); + paymentDetailData = new PaymentDetailData(id, paymentTypeData, null, null, null, null, null); + } + } + + savingsAccountTransactionData = SavingsAccountTransactionData.create(transactionId, transactionType, paymentDetailData, + id, accountNo, date, currency, amount, outstandingChargeAmount, runningBalance, reversed, transSubmittedOnDate, + postInterestAsOn, cumulativeBalance, balanceEndDate); + savingsAccountTransactionData.setOverdraftAmount(overdraftAmount); + + transMap.put("id", transactionId); + if (savingsAccountData.getOfficeId() == null) { + savingsAccountData.setOfficeId(officeId); + } + + savingsAccountData.setSavingsAccountTransactionData(savingsAccountTransactionData); + } + + if (chargeId != null && !chargeDetails.containsValue(chargeId)) { + final boolean isPenalty = rs.getBoolean("isPenaltyCharge"); + final BigDecimal chargeAmount = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "chargeAmount"); + final Integer chargesTimeType = rs.getInt("chargeTimeType"); + final EnumOptionData enumOptionDataForChargesTimeType = new EnumOptionData(chargesTimeType.longValue(), null, null); + final SavingsAccountChargeData savingsAccountChargeData = new SavingsAccountChargeData(chargeId, chargeAmount, + enumOptionDataForChargesTimeType, isPenalty); + + final Long chargesPaidById = rs.getLong("chargesPaidById"); + final BigDecimal chargesPaid = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "paidByAmount"); + final SavingsAccountChargesPaidByData savingsAccountChargesPaidByData = new SavingsAccountChargesPaidByData( + chargesPaidById, chargesPaid); + savingsAccountChargesPaidByData.setSavingsAccountChargeData(savingsAccountChargeData); + if (savingsAccountChargesPaidByData != null) { + savingsAccountTransactionData.setChargesPaidByData(savingsAccountChargesPaidByData); + } + + chargeDetails.put("id", chargeId); + } + + if (taxDetailId != null && !taxDetails.containsValue(taxDetailId)) { + final BigDecimal amount = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "taxAmount"); + final BigDecimal percentage = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "taxPercentage"); + final Long debitId = rs.getLong("debitAccountId"); + final Long creditId = rs.getLong("creditAccountId"); + final GLAccountData debitAccount = GLAccountData.createFrom(debitId); + final GLAccountData creditAccount = GLAccountData.createFrom(creditId); + + if (taxComponentId != null) { + final TaxComponentData taxComponent = TaxComponentData.createTaxComponent(taxComponentId, percentage, debitAccount, + creditAccount); + savingsAccountTransactionData.setTaxDetails(new TaxDetailsData(taxComponent, amount)); + } + + taxDetails.put("id", taxDetailId); + } + + } + if (savingsAccountData != null) { + savingsAccountDataList.add(savingsAccountData); + } + return savingsAccountDataList; + + // final String productName = rs.getString("productName"); + + // final String fieldOfficerName = rs.getString("fieldOfficerName"); + + // final String submittedByUsername = rs.getString("submittedByUsername"); + // final String submittedByFirstname = rs.getString("submittedByFirstname"); + // final String submittedByLastname = rs.getString("submittedByLastname"); + // + + // final String rejectedByUsername = rs.getString("rejectedByUsername"); + // final String rejectedByFirstname = rs.getString("rejectedByFirstname"); + // final String rejectedByLastname = rs.getString("rejectedByLastname"); + // + + // final String withdrawnByUsername = rs.getString("withdrawnByUsername"); + // final String withdrawnByFirstname = rs.getString("withdrawnByFirstname"); + // final String withdrawnByLastname = rs.getString("withdrawnByLastname"); + + // final String approvedByUsername = rs.getString("approvedByUsername"); + // final String approvedByFirstname = rs.getString("approvedByFirstname"); + // final String approvedByLastname = rs.getString("approvedByLastname"); + + // final String activatedByUsername = rs.getString("activatedByUsername"); + // final String activatedByFirstname = rs.getString("activatedByFirstname"); + // final String activatedByLastname = rs.getString("activatedByLastname"); + + // final String closedByUsername = rs.getString("closedByUsername"); + // final String closedByFirstname = rs.getString("closedByFirstname"); + // final String closedByLastname = rs.getString("closedByLastname"); + + // final String currencyName = rs.getString("currencyName"); + // final String currencyNameCode = rs.getString("currencyNameCode"); + // final String currencyDisplaySymbol = rs.getString("currencyDisplaySymbol"); + + /* + * final BigDecimal withdrawalFeeAmount = rs.getBigDecimal("withdrawalFeeAmount"); + * + * EnumOptionData withdrawalFeeType = null; final Integer withdrawalFeeTypeValue = + * JdbcSupport.getInteger(rs, "withdrawalFeeTypeEnum"); if (withdrawalFeeTypeValue != null) { + * withdrawalFeeType = SavingsEnumerations.withdrawalFeeType(withdrawalFeeTypeValue); } + */ + + /* + * final BigDecimal annualFeeAmount = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "annualFeeAmount"); + * + * MonthDay annualFeeOnMonthDay = null; final Integer annualFeeOnMonth = JdbcSupport.getInteger(rs, + * "annualFeeOnMonth"); final Integer annualFeeOnDay = JdbcSupport.getInteger(rs, "annualFeeOnDay"); if + * (annualFeeAmount != null && annualFeeOnDay != null) { annualFeeOnMonthDay = new + * MonthDay(annualFeeOnMonth, annualFeeOnDay); } + * + * final LocalDate annualFeeNextDueDate = JdbcSupport.getLocalDate(rs, "annualFeeNextDueDate"); + */ + + } + } + private static final class SavingAccountMapper implements RowMapper { private final String schemaSql; @@ -285,7 +790,6 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead // withdrawalFeeAmount,"); // sqlBuilder.append("sa.withdrawal_fee_type_enum as // withdrawalFeeTypeEnum, "); - sqlBuilder.append("sa.withdrawal_fee_for_transfer as withdrawalFeeForTransfers, "); sqlBuilder.append("sa.allow_overdraft as allowOverdraft, "); sqlBuilder.append("sa.overdraft_limit as overdraftLimit, "); sqlBuilder.append("sa.nominal_annual_interest_rate_overdraft as nominalAnnualInterestRateOverdraft, "); @@ -315,6 +819,7 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead sqlBuilder.append("sa.last_interest_calculation_date as lastInterestCalculationDate, "); sqlBuilder.append("sa.interest_posted_till_date as interestPostedTillDate, "); sqlBuilder.append("sa.total_savings_amount_on_hold as onHoldAmount, "); + sqlBuilder.append("sa.withdrawal_fee_for_transfer as withdrawalFeeForTransfers, "); sqlBuilder.append("tg.id as taxGroupId, tg.name as taxGroupName, "); sqlBuilder.append("(select IFNULL(max(sat.transaction_date),sa.activatedon_date) "); sqlBuilder.append("from m_savings_account_transaction as sat "); @@ -337,7 +842,7 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead sqlBuilder.append("left join m_appuser abu on abu.id = sa.approvedon_userid "); sqlBuilder.append("left join m_appuser avbu on avbu.id = sa.activatedon_userid "); sqlBuilder.append("left join m_appuser cbu on cbu.id = sa.closedon_userid "); - sqlBuilder.append("left join m_tax_group tg on tg.id = sa.tax_group_id "); + sqlBuilder.append("left join m_tax_group tg on tg.id = sa.tax_group_id "); this.schemaSql = sqlBuilder.toString(); } @@ -357,6 +862,7 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead final Long groupId = JdbcSupport.getLong(rs, "groupId"); final String groupName = rs.getString("groupName"); + final Long clientId = JdbcSupport.getLong(rs, "clientId"); final String clientName = rs.getString("clientName"); @@ -765,6 +1271,33 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead new Object[] { savingsId, depositAccountType.getValue(), transactionId }); } + private static final class SavingsAccountTransactionsForBatchMapper implements RowMapper { + + private final String schemaSql; + + SavingsAccountTransactionsForBatchMapper() { + + final StringBuilder sqlBuilder = new StringBuilder(400); + sqlBuilder.append("tr.id as transactionId, tr.ref_no as refNo "); + sqlBuilder.append("from m_savings_account_transaction tr"); + + this.schemaSql = sqlBuilder.toString(); + } + + public String schema() { + return this.schemaSql; + } + + @Override + public SavingsAccountTransactionData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { + final Long id = rs.getLong("transactionId"); + SavingsAccountTransactionData savingsAccountTransactionData = SavingsAccountTransactionData.create(id); + final String refNo = rs.getString("refNo"); + savingsAccountTransactionData.setRefNo(refNo); + return savingsAccountTransactionData; + } + } + /* * @Override public Collection retrieveAccountsWithAnnualFeeDue() { final String sql = * "select " + this.annualFeeMapper.schema() + diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformService.java index b72d60ca5..134460eb1 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformService.java @@ -25,6 +25,7 @@ import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.organisation.office.domain.Office; import org.apache.fineract.organisation.staff.domain.Staff; +import org.apache.fineract.portfolio.savings.data.SavingsAccountData; import org.apache.fineract.portfolio.savings.domain.SavingsAccount; import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction; @@ -87,6 +88,12 @@ public interface SavingsAccountWritePlatformService { void postInterest(SavingsAccount account, boolean postInterestAs, LocalDate transactionDate, boolean backdatedTxnsAllowedTill); + // SavingsAccountData postInterest(SavingsAccountData account, boolean postInterestAs, LocalDate transactionDate, + // boolean backdatedTxnsAllowedTill); + + SavingsAccountData postInterest(SavingsAccountData account, boolean postInterestAs, LocalDate transactionDate, + boolean backdatedTxnsAllowedTill); + CommandProcessingResult blockAccount(Long savingsId); CommandProcessingResult unblockAccount(Long savingsId); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java index 712ed12b2..eac3a8a97 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java @@ -55,6 +55,7 @@ import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRu import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.exception.PlatformServiceUnavailableException; import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.RoutingDataSource; import org.apache.fineract.infrastructure.dataqueries.data.EntityTables; import org.apache.fineract.infrastructure.dataqueries.data.StatusEnum; import org.apache.fineract.infrastructure.dataqueries.service.EntityDatatableChecksWritePlatformService; @@ -93,8 +94,10 @@ import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType; import org.apache.fineract.portfolio.savings.SavingsApiConstants; import org.apache.fineract.portfolio.savings.SavingsTransactionBooleanValues; import org.apache.fineract.portfolio.savings.data.SavingsAccountChargeDataValidator; +import org.apache.fineract.portfolio.savings.data.SavingsAccountData; import org.apache.fineract.portfolio.savings.data.SavingsAccountDataValidator; import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionDTO; +import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData; import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionDataValidator; import org.apache.fineract.portfolio.savings.domain.DepositAccountOnHoldTransaction; import org.apache.fineract.portfolio.savings.domain.DepositAccountOnHoldTransactionRepository; @@ -123,6 +126,7 @@ import org.apache.fineract.useradministration.domain.AppUserRepositoryWrapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; @@ -156,6 +160,8 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi private final StandingInstructionRepository standingInstructionRepository; private final BusinessEventNotifierService businessEventNotifierService; private final GSIMRepositoy gsimRepository; + private final JdbcTemplate jdbcTemplate; + private final SavingsAccountInterestPostingService savingsAccountInterestPostingService; @Autowired public SavingsAccountWritePlatformServiceJpaRepositoryImpl(final PlatformSecurityContext context, @@ -177,7 +183,8 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi final DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository, final EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService, final AppUserRepositoryWrapper appuserRepository, final StandingInstructionRepository standingInstructionRepository, - final BusinessEventNotifierService businessEventNotifierService, final GSIMRepositoy gsimRepository) { + final BusinessEventNotifierService businessEventNotifierService, final GSIMRepositoy gsimRepository, + final RoutingDataSource dataSource, final SavingsAccountInterestPostingService savingsAccountInterestPostingService) { this.context = context; this.savingAccountRepositoryWrapper = savingAccountRepositoryWrapper; this.savingsAccountTransactionRepository = savingsAccountTransactionRepository; @@ -204,6 +211,8 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi this.standingInstructionRepository = standingInstructionRepository; this.businessEventNotifierService = businessEventNotifierService; this.gsimRepository = gsimRepository; + this.jdbcTemplate = new JdbcTemplate(dataSource); + this.savingsAccountInterestPostingService = savingsAccountInterestPostingService; } private static final Logger LOG = LoggerFactory.getLogger(SavingsAccountWritePlatformServiceJpaRepositoryImpl.class); @@ -582,6 +591,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService .isSavingsInterestPostingAtCurrentPeriodEnd(); final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + if (account.getNominalAnnualInterestRate().compareTo(BigDecimal.ZERO) > 0 || (account.allowOverdraft() && account.getNominalAnnualInterestRateOverdraft().compareTo(BigDecimal.ZERO) > 0)) { final Set existingTransactionIds = new HashSet<>(); @@ -623,6 +633,52 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi } } + @Transactional + @Override + public SavingsAccountData postInterest(SavingsAccountData savingsAccountData, final boolean postInterestAs, + final LocalDate transactionDate, final boolean backdatedTxnsAllowedTill) { + + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + + if (savingsAccountData.getNominalAnnualInterestRate().compareTo(BigDecimal.ZERO) > 0 || (savingsAccountData.isAllowOverdraft() + && savingsAccountData.getNominalAnnualInterestRateOverdraft().compareTo(BigDecimal.ZERO) > 0)) { + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + updateExistingTransactionsDetails(savingsAccountData, existingTransactionIds, existingReversedTransactionIds); + + final LocalDate today = DateUtils.getLocalDateOfTenant(); + final MathContext mc = new MathContext(10, MoneyHelper.getRoundingMode()); + boolean isInterestTransfer = false; + LocalDate postInterestOnDate = null; + if (postInterestAs) { + postInterestOnDate = transactionDate; + } + + long startPosting = System.currentTimeMillis(); + LOG.info("Interest Posting Start Here at {}", startPosting); + + savingsAccountData = this.savingsAccountInterestPostingService.postInterest(mc, today, isInterestTransfer, + isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, + savingsAccountData); + long endPosting = System.currentTimeMillis(); + LOG.info("Interest Posting Ends within {}", endPosting - startPosting); + + if (!backdatedTxnsAllowedTill) { + List transactions = savingsAccountData.getTransactions(); + for (SavingsAccountTransactionData accountTransaction : transactions) { + if (accountTransaction.getId() == null) { + savingsAccountData.setNewSavingsAccountTransactionData(accountTransaction); + } + } + } + savingsAccountData.setExistingTransactionIds(existingTransactionIds); + savingsAccountData.setExistingReversedTransactionIds(existingReversedTransactionIds); + } + return savingsAccountData; + } + @Override public CommandProcessingResult undoTransaction(final Long savingsId, final Long transactionId, final boolean allowAccountTransferModification) { @@ -630,7 +686,6 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService .isSavingsInterestPostingAtCurrentPeriodEnd(); final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); final Set existingTransactionIds = new HashSet<>(); final Set existingReversedTransactionIds = new HashSet<>(); @@ -1394,12 +1449,25 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi existingReversedTransactionIds.addAll(account.findExistingReversedTransactionIds()); } + private void updateExistingTransactionsDetails(SavingsAccountData account, Set existingTransactionIds, + Set existingReversedTransactionIds) { + existingTransactionIds.addAll(account.findCurrentTransactionIdsWithPivotDateConfig()); + existingReversedTransactionIds.addAll(account.findCurrentReversedTransactionIdsWithPivotDateConfig()); + } + private void updateSavingsTransactionsDetails(SavingsAccount account, Set existingTransactionIds, Set existingReversedTransactionIds) { existingTransactionIds.addAll(account.findCurrentTransactionIdsWithPivotDateConfig()); existingReversedTransactionIds.addAll(account.findCurrentReversedTransactionIdsWithPivotDateConfig()); } + @SuppressWarnings("unused") + private void updateSavingsTransactionsDetails(SavingsAccountData account, Set existingTransactionIds, + Set existingReversedTransactionIds) { + existingTransactionIds.addAll(account.findCurrentTransactionIdsWithPivotDateConfig()); + existingReversedTransactionIds.addAll(account.findCurrentReversedTransactionIdsWithPivotDateConfig()); + } + private void postJournalEntries(final SavingsAccount savingsAccount, final Set existingTransactionIds, final Set existingReversedTransactionIds, final boolean backdatedTxnsAllowedTill) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsSchedularInterestPoster.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsSchedularInterestPoster.java index f5dde2555..3e0097e75 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsSchedularInterestPoster.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsSchedularInterestPoster.java @@ -19,25 +19,40 @@ package org.apache.fineract.portfolio.savings.service; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.math.BigDecimal; import java.security.SecureRandom; +import java.sql.Date; import java.time.LocalDate; import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; import java.util.List; +import java.util.UUID; import java.util.concurrent.Callable; +import org.apache.fineract.accounting.journalentry.domain.JournalEntryType; +import org.apache.fineract.batch.command.CommandStrategyProvider; +import org.apache.fineract.batch.service.ResolutionHelper; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant; +import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException; -import org.apache.fineract.portfolio.savings.domain.SavingsAccount; +import org.apache.fineract.portfolio.savings.data.SavingsAccountData; +import org.apache.fineract.portfolio.savings.data.SavingsAccountSummaryData; +import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData; import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler; import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Scope; import org.springframework.dao.CannotAcquireLockException; +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.orm.ObjectOptimisticLockingFailureException; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Isolation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionTemplate; /** * @author manoj @@ -49,22 +64,38 @@ public class SavingsSchedularInterestPoster implements Callable { private static final Logger LOG = LoggerFactory.getLogger(SavingsSchedularInterestPoster.class); private static final SecureRandom random = new SecureRandom(); + private static final String SAVINGS_TRANSACTION_IDENTIFIER = "S"; - private Collection savingsIds; + private Collection savingAccounts; private SavingsAccountWritePlatformService savingsAccountWritePlatformService; private SavingsAccountRepositoryWrapper savingsAccountRepository; private SavingsAccountAssembler savingAccountAssembler; private FineractPlatformTenant tenant; private ConfigurationDomainService configurationDomainService; + private boolean backdatedTxnsAllowedTill; + private List savingsAccountDataList = new ArrayList<>(); + private JdbcTemplate jdbcTemplate; + private TransactionTemplate transactionTemplate; + private CommandStrategyProvider strategyProvider; + private ResolutionHelper resolutionHelper; + private SavingsAccountReadPlatformService savingsAccountReadPlatformService; - public void setSavingsIds(Collection savingsIds) { - this.savingsIds = savingsIds; + public void setSavings(Collection savingAccounts) { + this.savingAccounts = savingAccounts; + } + + public void setBackdatedTxnsAllowedTill(final boolean backdatedTxnsAllowedTill) { + this.backdatedTxnsAllowedTill = backdatedTxnsAllowedTill; } public void setSavingsAccountWritePlatformService(SavingsAccountWritePlatformService savingsAccountWritePlatformService) { this.savingsAccountWritePlatformService = savingsAccountWritePlatformService; } + public void setSavingsAccountReadPlatformService(SavingsAccountReadPlatformService savingsAccountReadPlatformService) { + this.savingsAccountReadPlatformService = savingsAccountReadPlatformService; + } + public void setSavingsAccountRepository(SavingsAccountRepositoryWrapper savingsAccountRepository) { this.savingsAccountRepository = savingsAccountRepository; } @@ -77,6 +108,22 @@ public class SavingsSchedularInterestPoster implements Callable { this.tenant = tenant; } + public void setJdbcTemplate(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public void setTransactionTemplate(TransactionTemplate transactionTemplate) { + this.transactionTemplate = transactionTemplate; + } + + public void setResolutionHelper(ResolutionHelper resolutionHelper) { + this.resolutionHelper = resolutionHelper; + } + + public void setStrategyProvider(CommandStrategyProvider commandStrategyProvider) { + this.strategyProvider = commandStrategyProvider; + } + @Override @SuppressFBWarnings(value = { "DMI_RANDOM_USED_ONLY_ONCE" }, justification = "False positive for random object created and used only once") @@ -84,31 +131,38 @@ public class SavingsSchedularInterestPoster implements Callable { ThreadLocalContextUtil.setTenant(tenant); Integer maxNumberOfRetries = tenant.getConnection().getMaxRetriesOnDeadlock(); Integer maxIntervalBetweenRetries = tenant.getConnection().getMaxIntervalBetweenRetries(); - final boolean backdatedTxnsAllowedTill = this.configurationDomainService.retrievePivotDateConfig(); + + // List responseList = new ArrayList<>(); + long start = System.currentTimeMillis(); + LOG.info("Thread Execution Started at {}", start); + + List errors = new ArrayList<>(); int i = 0; - if (!savingsIds.isEmpty()) { - List errors = new ArrayList<>(); - for (Long savingsId : savingsIds) { - LOG.info("Savings ID {}", savingsId); + if (!savingAccounts.isEmpty()) { + for (SavingsAccountData savingsAccountData : savingAccounts) { Integer numberOfRetries = 0; while (numberOfRetries <= maxNumberOfRetries) { try { - SavingsAccount savingsAccount = this.savingsAccountRepository.findOneWithNotFoundDetection(savingsId); - this.savingAccountAssembler.assignSavingAccountHelpers(savingsAccount); boolean postInterestAsOn = false; LocalDate transactionDate = null; - this.savingsAccountWritePlatformService.postInterest(savingsAccount, postInterestAsOn, transactionDate, - backdatedTxnsAllowedTill); + long startPosting = System.currentTimeMillis(); + SavingsAccountData savingsAccountDataRet = savingsAccountWritePlatformService.postInterest(savingsAccountData, + postInterestAsOn, transactionDate, backdatedTxnsAllowedTill); + long endPosting = System.currentTimeMillis(); + savingsAccountDataList.add(savingsAccountDataRet); + + LOG.info("Posting Completed Within {}", endPosting - startPosting); numberOfRetries = maxNumberOfRetries + 1; } catch (CannotAcquireLockException | ObjectOptimisticLockingFailureException exception) { - LOG.info("Interest posting job for savings ID {} has been retried {} time(s)", savingsId, numberOfRetries); + LOG.info("Interest posting job for savings ID {} has been retried {} time(s)", savingsAccountData.getId(), + numberOfRetries); // Fail if the transaction has been retired for // maxNumberOfRetries if (numberOfRetries >= maxNumberOfRetries) { LOG.error( "Interest posting job for savings ID {} has been retried for the max allowed attempts of {} and will be rolled back", - savingsId, numberOfRetries); + savingsAccountData.getId(), numberOfRetries); errors.add(exception); break; } @@ -124,18 +178,195 @@ public class SavingsSchedularInterestPoster implements Callable { break; } } catch (Exception e) { - LOG.error("Interest posting job for savings failed for account {}", savingsId, e); + LOG.error("Interest posting job for savings failed for account {}", savingsAccountData.getId(), e); numberOfRetries = maxNumberOfRetries + 1; errors.add(e); } - i++; } - LOG.info("Savings count {}", i); + i++; } + + if (errors.isEmpty()) { + try { + batchUpdate(savingsAccountDataList); + } catch (DataAccessException exception) { + LOG.error("Batch update failed due to DataAccessException", exception); + errors.add(exception); + } catch (NullPointerException exception) { + LOG.error("Batch update failed due to NullPointerException", exception); + errors.add(exception); + } + } + if (!errors.isEmpty()) { throw new JobExecutionException(errors); } } + + long end = System.currentTimeMillis(); + LOG.info("Time To Finish the batch {} by thread {} for accounts {}", end - start, Thread.currentThread().getId(), + savingAccounts.size()); return null; } + + @Transactional(isolation = Isolation.READ_UNCOMMITTED, rollbackFor = Exception.class) + private void batchUpdateJournalEntries(final List savingsAccountDataList, + final HashMap savingsAccountTransactionDataHashMap) + throws DataAccessException, NullPointerException { + String queryForJGLUpdate = batchQueryForJournalEntries(); + List paramsForGLInsertion = new ArrayList<>(); + for (SavingsAccountData savingsAccountData : savingsAccountDataList) { + String currencyCode = savingsAccountData.getCurrency().getCode(); + + List savingsAccountTransactionDataList = savingsAccountData.getSavingsAccountTransactionData(); + for (SavingsAccountTransactionData savingsAccountTransactionData : savingsAccountTransactionDataList) { + if (savingsAccountTransactionData.getId() == null) { + final String key = savingsAccountTransactionData.getRefNo(); + if (savingsAccountTransactionDataHashMap.containsKey(key)) { + LocalDate currentDate = DateUtils.getLocalDateOfTenant(); + final SavingsAccountTransactionData dataFromFetch = savingsAccountTransactionDataHashMap.get(key); + savingsAccountTransactionData.setId(dataFromFetch.getId()); + + paramsForGLInsertion.add( + new Object[] { savingsAccountData.getGlAccountIdForSavingsControl(), savingsAccountData.getOfficeId(), null, + currencyCode, SAVINGS_TRANSACTION_IDENTIFIER + savingsAccountTransactionData.getId().toString(), + savingsAccountTransactionData.getId(), null, false, null, false, + Date.from(savingsAccountTransactionData.getTransactionDate() + .atStartOfDay(DateUtils.getDateTimeZoneOfTenant()).toInstant()), + JournalEntryType.CREDIT.getValue().longValue(), savingsAccountTransactionData.getAmount(), null, + JournalEntryType.CREDIT.getValue().longValue(), savingsAccountData.getId(), + Date.from(currentDate.atStartOfDay(DateUtils.getDateTimeZoneOfTenant()).toInstant()), + Date.from(currentDate.atStartOfDay(DateUtils.getDateTimeZoneOfTenant()).toInstant()), false, + BigDecimal.ZERO, BigDecimal.ZERO, null, + Date.from(savingsAccountTransactionData.getTransactionDate() + .atStartOfDay(DateUtils.getDateTimeZoneOfTenant()).toInstant()), + null, Integer.valueOf(1), Integer.valueOf(1) }); + + paramsForGLInsertion.add(new Object[] { savingsAccountData.getGlAccountIdForInterestOnSavings(), + savingsAccountData.getOfficeId(), null, currencyCode, + SAVINGS_TRANSACTION_IDENTIFIER + savingsAccountTransactionData.getId().toString(), + savingsAccountTransactionData.getId(), null, false, null, false, + Date.from(savingsAccountTransactionData.getTransactionDate() + .atStartOfDay(DateUtils.getDateTimeZoneOfTenant()).toInstant()), + JournalEntryType.DEBIT.getValue().longValue(), savingsAccountTransactionData.getAmount(), null, + JournalEntryType.DEBIT.getValue().longValue(), savingsAccountData.getId(), + Date.from(currentDate.atStartOfDay(DateUtils.getDateTimeZoneOfTenant()).toInstant()), + Date.from(currentDate.atStartOfDay(DateUtils.getDateTimeZoneOfTenant()).toInstant()), false, + BigDecimal.ZERO, BigDecimal.ZERO, null, + Date.from(savingsAccountTransactionData.getTransactionDate() + .atStartOfDay(DateUtils.getDateTimeZoneOfTenant()).toInstant()), + null, Integer.valueOf(1), Integer.valueOf(1) }); + + } + } + } + } + + if (paramsForGLInsertion != null && paramsForGLInsertion.size() > 0) { + this.jdbcTemplate.batchUpdate(queryForJGLUpdate, paramsForGLInsertion); + } + } + + private String batchQueryForJournalEntries() { + StringBuilder query = new StringBuilder(100); + + query.append("INSERT INTO acc_gl_journal_entry(account_id,office_id,reversal_id,currency_code,transaction_id,"); + query.append("savings_transaction_id,client_transaction_id,reversed,ref_num,manual_entry,entry_date,type_enum,"); + query.append("amount,description,entity_type_enum,entity_id,created_date,"); + query.append("lastmodified_date,is_running_balance_calculated,office_running_balance,organization_running_balance,"); + query.append("payment_details_id,transaction_date,share_transaction_id, createdby_id, lastmodifiedby_id) "); + query.append("VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); + + return query.toString(); + } + + private List fetchTransactionsFromIds(final List refNo) throws DataAccessException { + return this.savingsAccountReadPlatformService.retrieveAllTransactionData(refNo); + } + + @Transactional(isolation = Isolation.READ_UNCOMMITTED) + @SuppressWarnings("unused") + private void batchUpdate(final List savingsAccountDataList) throws DataAccessException { + String queryForSavingsUpdate = batchQueryForSavingsSummaryUpdate(); + String queryForTransactionInsertion = batchQueryForTransactionInsertion(); + List paramsForTransactionInsertion = new ArrayList<>(); + List paramsForSavingsSummary = new ArrayList<>(); + List transRefNo = new ArrayList<>(); + for (SavingsAccountData savingsAccountData : savingsAccountDataList) { + SavingsAccountSummaryData savingsAccountSummaryData = savingsAccountData.getSummary(); + paramsForSavingsSummary.add(new Object[] { savingsAccountSummaryData.getTotalDeposits(), + savingsAccountSummaryData.getTotalWithdrawals(), savingsAccountSummaryData.getTotalInterestEarned(), + savingsAccountSummaryData.getTotalInterestPosted(), savingsAccountSummaryData.getTotalWithdrawalFees(), + savingsAccountSummaryData.getTotalFeeCharge(), savingsAccountSummaryData.getTotalPenaltyCharge(), + savingsAccountSummaryData.getTotalAnnualFees(), savingsAccountSummaryData.getAvailableBalance(), + savingsAccountSummaryData.getTotalOverdraftInterestDerived(), savingsAccountSummaryData.getTotalWithholdTax(), + Date.from(savingsAccountSummaryData.getLastInterestCalculationDate().atStartOfDay(DateUtils.getDateTimeZoneOfTenant()) + .toInstant()), + Date.from(savingsAccountSummaryData.getInterestPostedTillDate().atStartOfDay(DateUtils.getDateTimeZoneOfTenant()) + .toInstant()), + savingsAccountData.getId() }); + List savingsAccountTransactionDataList = savingsAccountData.getSavingsAccountTransactionData(); + for (SavingsAccountTransactionData savingsAccountTransactionData : savingsAccountTransactionDataList) { + if (savingsAccountTransactionData.getId() == null) { + UUID uuid = UUID.randomUUID(); + savingsAccountTransactionData.setRefNo(uuid.toString()); + transRefNo.add(uuid.toString()); + java.util.Date balanceEndDate = null; + if (savingsAccountTransactionData.getBalanceEndDate() != null) { + balanceEndDate = Date.from(savingsAccountTransactionData.getBalanceEndDate() + .atStartOfDay(DateUtils.getDateTimeZoneOfTenant()).toInstant()); + } + paramsForTransactionInsertion.add(new Object[] { savingsAccountData.getId(), savingsAccountData.getOfficeId(), + savingsAccountTransactionData.getTransactionType().getId(), + Date.from(savingsAccountTransactionData.getTransactionDate().atStartOfDay(DateUtils.getDateTimeZoneOfTenant()) + .toInstant()), + savingsAccountTransactionData.getAmount(), balanceEndDate, + savingsAccountTransactionData.getBalanceNumberOfDays(), savingsAccountTransactionData.getRunningBalance(), + savingsAccountTransactionData.getCumulativeBalance(), savingsAccountTransactionData.getSubmittedOnDate(), + Integer.valueOf(1), savingsAccountTransactionData.isManualTransaction(), + savingsAccountTransactionData.getRefNo() }); + } + } + savingsAccountData.setUpdatedTransactions(savingsAccountTransactionDataList); + } + + if (transRefNo.size() > 0) { + this.jdbcTemplate.batchUpdate(queryForSavingsUpdate, paramsForSavingsSummary); + this.jdbcTemplate.batchUpdate(queryForTransactionInsertion, paramsForTransactionInsertion); + + List savingsAccountTransactionDataList = fetchTransactionsFromIds(transRefNo); + if (savingsAccountDataList != null) { + LOG.info("Fetched Transactions from DB: {}", savingsAccountTransactionDataList.size()); + } + + HashMap savingsAccountTransactionMap = new HashMap<>(); + for (SavingsAccountTransactionData savingsAccountTransactionData : savingsAccountTransactionDataList) { + final String key = savingsAccountTransactionData.getRefNo(); + savingsAccountTransactionMap.put(key, savingsAccountTransactionData); + } + batchUpdateJournalEntries(savingsAccountDataList, savingsAccountTransactionMap); + } + + } + + private String batchQueryForTransactionInsertion() { + StringBuilder query = new StringBuilder(100); + query.append("INSERT INTO m_savings_account_transaction (savings_account_id, office_id, is_reversed,"); + query.append("transaction_type_enum, transaction_date, amount, balance_end_date_derived,"); + query.append("balance_number_of_days_derived, running_balance_derived, cumulative_balance_derived,"); + query.append("created_date, appuser_id, is_manual, is_loan_disbursement, ref_no) VALUES "); + query.append("(?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?)"); + return query.toString(); + + } + + private String batchQueryForSavingsSummaryUpdate() { + StringBuilder query = new StringBuilder(100); + query.append("update m_savings_account set total_deposits_derived=?, total_withdrawals_derived=?, "); + query.append("total_interest_earned_derived=?, total_interest_posted_derived=?, total_withdrawal_fees_derived=?, "); + query.append("total_fees_charge_derived=?, total_penalty_charge_derived=?, total_annual_fees_derived=?, "); + query.append("account_balance_derived=?, total_overdraft_interest_derived=?, total_withhold_tax_derived=?, "); + query.append("last_interest_calculation_date=?, interest_posted_till_date=? where id=?"); + return query.toString(); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsSchedularServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsSchedularServiceImpl.java index c6844403d..e8d1b30bf 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsSchedularServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsSchedularServiceImpl.java @@ -21,28 +21,36 @@ package org.apache.fineract.portfolio.savings.service; import static org.apache.fineract.portfolio.savings.domain.SavingsAccountStatusType.ACTIVE; import java.time.LocalDate; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Queue; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; +import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant; import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.RoutingDataSource; import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; import org.apache.fineract.infrastructure.jobs.annotation.CronTarget; import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException; import org.apache.fineract.infrastructure.jobs.service.JobName; +import org.apache.fineract.portfolio.savings.data.SavingsAccountData; import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler; import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; -import org.springframework.util.CollectionUtils; +import org.springframework.transaction.support.TransactionTemplate; @Service public class SavingsSchedularServiceImpl implements SavingsSchedularService { @@ -54,17 +62,27 @@ public class SavingsSchedularServiceImpl implements SavingsSchedularService { private final SavingsAccountReadPlatformService savingAccountReadPlatformService; private final SavingsAccountRepositoryWrapper savingsAccountRepository; private final ApplicationContext applicationContext; + private final ConfigurationDomainService configurationDomainService; + private final JdbcTemplate jdbcTemplate; + private final TransactionTemplate transactionTemplate; + private Queue> queue = new ArrayDeque<>(); + private int queueSize = 1; @Autowired public SavingsSchedularServiceImpl(final SavingsAccountAssembler savingAccountAssembler, final SavingsAccountWritePlatformService savingsAccountWritePlatformService, final SavingsAccountReadPlatformService savingAccountReadPlatformService, - final SavingsAccountRepositoryWrapper savingsAccountRepository, final ApplicationContext applicationContext) { + final SavingsAccountRepositoryWrapper savingsAccountRepository, final ApplicationContext applicationContext, + final ConfigurationDomainService configurationDomainService, final RoutingDataSource dataSource, + final TransactionTemplate transactionTemplate) { this.savingAccountAssembler = savingAccountAssembler; this.savingsAccountWritePlatformService = savingsAccountWritePlatformService; this.savingAccountReadPlatformService = savingAccountReadPlatformService; this.savingsAccountRepository = savingsAccountRepository; this.applicationContext = applicationContext; + this.configurationDomainService = configurationDomainService; + this.jdbcTemplate = new JdbcTemplate(dataSource); + this.transactionTemplate = transactionTemplate; } @Override @@ -77,28 +95,43 @@ public class SavingsSchedularServiceImpl implements SavingsSchedularService { Long maxSavingsIdInList = 0L; // initialise the executor service with fetched configurations final ExecutorService executorService = Executors.newFixedThreadPool(threadPoolSize); + final boolean backdatedTxnsAllowedTill = this.configurationDomainService.retrievePivotDateConfig(); - List savingsIds = Collections.synchronizedList( - this.savingAccountReadPlatformService.getAccountsIdsByStatusPaged(ACTIVE.getValue(), pageSize, maxSavingsIdInList)); - if (!CollectionUtils.isEmpty(savingsIds)) { - do { - int totalFilteredRecords = savingsIds.size(); - LOG.info("Starting Interest posting - total filtered records - {}", totalFilteredRecords); - postInterest(savingsIds, threadPoolSize, batchSize, executorService); - maxSavingsIdInList = savingsIds.get(savingsIds.size() - 1); - savingsIds = Collections.synchronizedList( - this.savingAccountReadPlatformService.getAccountsIdsByStatusPaged(ACTIVE.getValue(), pageSize, maxSavingsIdInList)); - } while (!CollectionUtils.isEmpty(savingsIds)); + long start = System.currentTimeMillis(); + + LOG.info("Reading Savings Account Data!"); + List savingsAccounts = this.savingAccountReadPlatformService + .retrieveAllSavingsDataForInterestPosting(backdatedTxnsAllowedTill, pageSize, ACTIVE.getValue(), maxSavingsIdInList); + + if (savingsAccounts != null && savingsAccounts.size() > 0) { + savingsAccounts = Collections.synchronizedList(savingsAccounts); + long finish = System.currentTimeMillis(); + LOG.info("Done fetching Data within {} milliseconds", finish - start); + if (savingsAccounts != null) { + queue.add(savingsAccounts); + } + + if (!CollectionUtils.isEmpty(queue)) { + do { + int totalFilteredRecords = savingsAccounts.size(); + LOG.info("Starting Interest posting - total records - {}", totalFilteredRecords); + List queueElement = queue.element(); + maxSavingsIdInList = queueElement.get(queueElement.size() - 1).getId(); + postInterest(queue.remove(), threadPoolSize, batchSize, executorService, backdatedTxnsAllowedTill, pageSize, + maxSavingsIdInList); + } while (!CollectionUtils.isEmpty(queue)); + } + // shutdown the executor when done + executorService.shutdownNow(); } - // shutdown the executor when done - executorService.shutdownNow(); } - private void postInterest(List savingsIds, int threadPoolSize, int batchSize, ExecutorService executorService) { + private void postInterest(List savingsAccounts, int threadPoolSize, int batchSize, ExecutorService executorService, + final boolean backdatedTxnsAllowedTill, final int pageSize, Long maxSavingsIdInList) { List> posters = new ArrayList<>(); int fromIndex = 0; // get the size of current paginated dataset - int size = savingsIds.size(); + int size = savingsAccounts.size(); // calculate the batch size batchSize = (int) Math.ceil((double) size / threadPoolSize); @@ -107,21 +140,50 @@ public class SavingsSchedularServiceImpl implements SavingsSchedularService { } int toIndex = (batchSize > size - 1) ? size : batchSize; - while (toIndex < size && savingsIds.get(toIndex - 1).equals(savingsIds.get(toIndex))) { + while (toIndex < size && savingsAccounts.get(toIndex - 1).getId().equals(savingsAccounts.get(toIndex).getId())) { toIndex++; } boolean lastBatch = false; int loopCount = size / batchSize + 1; + FineractPlatformTenant tenant = ThreadLocalContextUtil.getTenant(); + Long finalMaxSavingsIdInList = maxSavingsIdInList; + + Callable fetchData = () -> { + ThreadLocalContextUtil.setTenant(tenant); + Long maxId = finalMaxSavingsIdInList; + if (!queue.isEmpty()) { + maxId = Math.max(finalMaxSavingsIdInList, queue.element().get(queue.element().size() - 1).getId()); + } + + while (queue.size() <= queueSize) { + LOG.info("Fetching while threads are running!"); + List savingsAccountDataList = Collections.synchronizedList(this.savingAccountReadPlatformService + .retrieveAllSavingsDataForInterestPosting(backdatedTxnsAllowedTill, pageSize, ACTIVE.getValue(), maxId)); + if (savingsAccountDataList == null || savingsAccountDataList.isEmpty()) { + break; + } + maxId = savingsAccountDataList.get(savingsAccountDataList.size() - 1).getId(); + queue.add(savingsAccountDataList); + } + return null; + }; + posters.add(fetchData); + for (long i = 0; i < loopCount; i++) { - List subList = safeSubList(savingsIds, fromIndex, toIndex); + List subList = safeSubList(savingsAccounts, fromIndex, toIndex); SavingsSchedularInterestPoster poster = (SavingsSchedularInterestPoster) this.applicationContext .getBean("savingsSchedularInterestPoster"); - poster.setSavingsIds(subList); - poster.setTenant(ThreadLocalContextUtil.getTenant()); + poster.setSavings(subList); + poster.setTenant(tenant); poster.setSavingsAccountWritePlatformService(savingsAccountWritePlatformService); + poster.setSavingsAccountReadPlatformService(savingAccountReadPlatformService); poster.setSavingsAccountRepository(savingsAccountRepository); poster.setSavingAccountAssembler(savingAccountAssembler); + poster.setJdbcTemplate(jdbcTemplate); + poster.setBackdatedTxnsAllowedTill(backdatedTxnsAllowedTill); + poster.setTransactionTemplate(transactionTemplate); + posters.add(poster); if (lastBatch) { @@ -132,13 +194,32 @@ public class SavingsSchedularServiceImpl implements SavingsSchedularService { } fromIndex = fromIndex + (toIndex - fromIndex); toIndex = (toIndex + batchSize > size - 1) ? size : toIndex + batchSize; - while (toIndex < size && savingsIds.get(toIndex - 1).equals(savingsIds.get(toIndex))) { + while (toIndex < size && savingsAccounts.get(toIndex - 1).getId().equals(savingsAccounts.get(toIndex).getId())) { toIndex++; } } + try { List> responses = executorService.invokeAll(posters); + Long maxId = maxSavingsIdInList; + if (!queue.isEmpty()) { + maxId = Math.max(maxSavingsIdInList, queue.element().get(queue.element().size() - 1).getId()); + } + + while (queue.size() <= queueSize) { + LOG.info("Fetching while threads are running!..:: this is not supposed to run........"); + savingsAccounts = Collections.synchronizedList(this.savingAccountReadPlatformService + .retrieveAllSavingsDataForInterestPosting(backdatedTxnsAllowedTill, pageSize, ACTIVE.getValue(), maxId)); + if (savingsAccounts == null || savingsAccounts.isEmpty()) { + break; + } + maxId = savingsAccounts.get(savingsAccounts.size() - 1).getId(); + LOG.info("Add to the Queue"); + queue.add(savingsAccounts); + } + checkCompletion(responses); + LOG.info("Queue size {}", queue.size()); } catch (InterruptedException e1) { LOG.error("Interrupted while postInterest", e1); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/tax/data/TaxComponentData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/tax/data/TaxComponentData.java index f0cc2e77a..93c38ca68 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/tax/data/TaxComponentData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/tax/data/TaxComponentData.java @@ -109,4 +109,60 @@ public final class TaxComponentData implements Serializable { this.glAccountTypeOptions = glAccountTypeOptions; } + private TaxComponentData(final Long id, final BigDecimal percentage, final GLAccountData debitAcount, + final GLAccountData creditAcount) { + this.id = id; + this.percentage = percentage; + this.name = null; + this.debitAccountType = null; + this.debitAccount = debitAcount; + this.creditAccountType = null; + this.creditAccount = creditAcount; + this.startDate = null; + this.taxComponentHistories = null; + this.glAccountOptions = null; + this.glAccountTypeOptions = null; + } + + public static TaxComponentData createTaxComponent(final Long id, final BigDecimal percentage, final GLAccountData debitAccount, + final GLAccountData creditAccount) { + return new TaxComponentData(id, percentage, debitAccount, creditAccount); + } + + public BigDecimal getApplicablePercentage(final LocalDate date) { + BigDecimal percentage = null; + if (occursOnDayFrom(date)) { + percentage = getPercentage(); + } else { + for (TaxComponentHistoryData componentHistory : this.taxComponentHistories) { + if (componentHistory.occursOnDayFromAndUpToAndIncluding(date)) { + percentage = componentHistory.getPercentage(); + break; + } + } + } + return percentage; + } + + public BigDecimal getPercentage() { + return this.percentage; + } + + private boolean occursOnDayFrom(final LocalDate target) { + return target != null && target.isAfter(startDate()); + } + + public LocalDate startDate() { + LocalDate startDate = null; + if (this.startDate != null) { + startDate = this.startDate; + // startDate = LocalDate.ofInstant(this.startDate.toInstant(), DateUtils.getDateTimeZoneOfTenant()); + } + return startDate; + } + + public GLAccountData getCreditAcount() { + return this.creditAccount; + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/tax/data/TaxComponentHistoryData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/tax/data/TaxComponentHistoryData.java index 87000cd1c..086f65d40 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/tax/data/TaxComponentHistoryData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/tax/data/TaxComponentHistoryData.java @@ -36,4 +36,31 @@ public class TaxComponentHistoryData implements Serializable { this.startDate = startDate; this.endDate = endDate; } + + public boolean occursOnDayFromAndUpToAndIncluding(final LocalDate target) { + if (this.endDate == null) { + return target != null && target.isAfter(startDate()); + } + return target != null && target.isAfter(startDate()) && !target.isAfter(endDate()); + } + + public LocalDate startDate() { + LocalDate startDate = null; + if (this.startDate != null) { + startDate = this.startDate; + } + return startDate; + } + + public LocalDate endDate() { + LocalDate endDate = null; + if (this.endDate != null) { + endDate = this.endDate; + } + return endDate; + } + + public BigDecimal getPercentage() { + return this.percentage; + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/tax/data/TaxDetailsData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/tax/data/TaxDetailsData.java new file mode 100644 index 000000000..d0cdb5dfa --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/tax/data/TaxDetailsData.java @@ -0,0 +1,49 @@ +/** + * 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.tax.data; + +import java.math.BigDecimal; +import org.apache.fineract.organisation.monetary.domain.Money; + +public class TaxDetailsData { + + private TaxComponentData taxComponent; + + private BigDecimal amount; + + protected TaxDetailsData() {} + + public TaxDetailsData(final TaxComponentData taxComponent, final BigDecimal amount) { + this.taxComponent = taxComponent; + this.amount = amount; + } + + public TaxComponentData getTaxComponent() { + return this.taxComponent; + } + + public BigDecimal getAmount() { + return this.amount; + } + + public void updateAmount(Money amount) { + this.amount = amount.getAmount(); + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/tax/data/TaxGroupData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/tax/data/TaxGroupData.java index d42f713b4..dfb517943 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/tax/data/TaxGroupData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/tax/data/TaxGroupData.java @@ -61,4 +61,8 @@ public final class TaxGroupData implements Serializable { this.taxComponents = taxComponents; } + public Collection getTaxAssociations() { + return this.taxAssociations; + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/tax/data/TaxGroupMappingsData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/tax/data/TaxGroupMappingsData.java index b98b38f3d..f0009cd8f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/tax/data/TaxGroupMappingsData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/tax/data/TaxGroupMappingsData.java @@ -38,4 +38,31 @@ public class TaxGroupMappingsData implements Serializable { this.startDate = startDate; this.endDate = endDate; } + + public TaxComponentData getTaxComponent() { + return this.taxComponent; + } + + public boolean occursOnDayFromAndUpToAndIncluding(final LocalDate target) { + if (this.endDate == null) { + return target != null && target.isAfter(startDate()); + } + return target != null && target.isAfter(startDate()) && !target.isAfter(endDate()); + } + + public LocalDate startDate() { + LocalDate startDate = null; + if (this.startDate != null) { + startDate = this.startDate; + } + return startDate; + } + + public LocalDate endDate() { + LocalDate endDate = null; + if (this.endDate != null) { + endDate = this.endDate; + } + return endDate; + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/tax/service/TaxUtils.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/tax/service/TaxUtils.java index fcc8242c3..ee5b0cd10 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/tax/service/TaxUtils.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/tax/service/TaxUtils.java @@ -25,6 +25,8 @@ import java.util.List; import java.util.Map; import java.util.Set; import org.apache.fineract.organisation.monetary.domain.MoneyHelper; +import org.apache.fineract.portfolio.tax.data.TaxComponentData; +import org.apache.fineract.portfolio.tax.data.TaxGroupMappingsData; import org.apache.fineract.portfolio.tax.domain.TaxComponent; import org.apache.fineract.portfolio.tax.domain.TaxGroupMappings; @@ -55,6 +57,27 @@ public final class TaxUtils { return map; } + public static Map splitTaxData(final BigDecimal amount, final LocalDate date, + final Set taxGroupMappings, final int scale) { + Map map = new HashMap<>(3); + if (amount != null) { + final double amountVal = amount.doubleValue(); + double cent_percentage = Double.parseDouble("100.0"); + for (TaxGroupMappingsData groupMappings : taxGroupMappings) { + if (groupMappings.occursOnDayFromAndUpToAndIncluding(date)) { + TaxComponentData component = groupMappings.getTaxComponent(); + BigDecimal percentage = component.getApplicablePercentage(date); + if (percentage != null) { + double percentageVal = percentage.doubleValue(); + double tax = amountVal * percentageVal / cent_percentage; + map.put(component, BigDecimal.valueOf(tax).setScale(scale, MoneyHelper.getRoundingMode())); + } + } + } + } + return map; + } + public static BigDecimal incomeAmount(final BigDecimal amount, final LocalDate date, final Set taxGroupMappings, final int scale) { Map map = splitTax(amount, date, taxGroupMappings, scale); @@ -74,6 +97,14 @@ public final class TaxUtils { return totalTax; } + public static BigDecimal totalTaxDataAmount(final Map map) { + BigDecimal totalTax = BigDecimal.ZERO; + for (BigDecimal tax : map.values()) { + totalTax = totalTax.add(tax); + } + return totalTax; + } + public static BigDecimal addTax(final BigDecimal amount, final LocalDate date, final List taxGroupMappings, final int scale) { BigDecimal totalAmount = null; diff --git a/fineract-provider/src/main/resources/sql/migrations/core_db/V391__add_transaction_ref_column.sql b/fineract-provider/src/main/resources/sql/migrations/core_db/V391__add_transaction_ref_column.sql new file mode 100644 index 000000000..d94b2d4b6 --- /dev/null +++ b/fineract-provider/src/main/resources/sql/migrations/core_db/V391__add_transaction_ref_column.sql @@ -0,0 +1,22 @@ +-- +-- 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. +-- + +ALTER TABLE `m_savings_account_transaction` ADD COLUMN `ref_no` VARCHAR(128) DEFAULT NULL; + +ALTER TABLE `m_savings_account_transaction` ADD UNIQUE INDEX `transaction_ref_no` (`ref_no`);