blob: 35061831131feec4f8e6ff09a05ad96249b6e2cd [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.savings.service;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.SAVINGS_ACCOUNT_CHARGE_RESOURCE_NAME;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.amountParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.chargeIdParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.dueAsOfDateParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.lienAllowedParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.transactionAmountParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.transactionDateParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.withHoldTaxParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.withdrawBalanceParamName;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import io.github.resilience4j.retry.annotation.Retry;
import java.math.BigDecimal;
import java.math.MathContext;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.core.exception.PlatformServiceUnavailableException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.infrastructure.dataqueries.data.EntityTables;
import org.apache.fineract.infrastructure.dataqueries.data.StatusEnum;
import org.apache.fineract.infrastructure.dataqueries.service.EntityDatatableChecksWritePlatformService;
import org.apache.fineract.infrastructure.event.business.domain.savings.SavingsActivateBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.savings.SavingsCloseBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.savings.SavingsPostInterestBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.organisation.holiday.domain.HolidayRepositoryWrapper;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.organisation.staff.domain.Staff;
import org.apache.fineract.organisation.staff.domain.StaffRepositoryWrapper;
import org.apache.fineract.organisation.workingdays.domain.WorkingDaysRepositoryWrapper;
import org.apache.fineract.portfolio.account.PortfolioAccountType;
import org.apache.fineract.portfolio.account.domain.AccountTransferStandingInstruction;
import org.apache.fineract.portfolio.account.domain.StandingInstructionRepository;
import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus;
import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlatformService;
import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService;
import org.apache.fineract.portfolio.charge.domain.Charge;
import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper;
import org.apache.fineract.portfolio.charge.domain.ChargeTimeType;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.client.exception.ClientNotActiveException;
import org.apache.fineract.portfolio.group.domain.Group;
import org.apache.fineract.portfolio.group.exception.GroupNotActiveException;
import org.apache.fineract.portfolio.note.domain.Note;
import org.apache.fineract.portfolio.note.domain.NoteRepository;
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService;
import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType;
import org.apache.fineract.portfolio.savings.SavingsApiConstants;
import org.apache.fineract.portfolio.savings.SavingsTransactionBooleanValues;
import org.apache.fineract.portfolio.savings.data.SavingsAccountChargeDataValidator;
import org.apache.fineract.portfolio.savings.data.SavingsAccountData;
import org.apache.fineract.portfolio.savings.data.SavingsAccountDataValidator;
import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionDTO;
import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData;
import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionDataValidator;
import org.apache.fineract.portfolio.savings.domain.DepositAccountOnHoldTransaction;
import org.apache.fineract.portfolio.savings.domain.DepositAccountOnHoldTransactionRepository;
import org.apache.fineract.portfolio.savings.domain.GSIMRepositoy;
import org.apache.fineract.portfolio.savings.domain.GroupSavingsIndividualMonitoring;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountCharge;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountChargeRepositoryWrapper;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountDomainService;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountStatusType;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransactionRepository;
import org.apache.fineract.portfolio.savings.exception.PostInterestAsOnDateException;
import org.apache.fineract.portfolio.savings.exception.PostInterestAsOnDateException.PostInterestAsOnExceptionType;
import org.apache.fineract.portfolio.savings.exception.PostInterestClosingDateException;
import org.apache.fineract.portfolio.savings.exception.SavingsAccountClosingNotAllowedException;
import org.apache.fineract.portfolio.savings.exception.SavingsAccountTransactionNotFoundException;
import org.apache.fineract.portfolio.savings.exception.SavingsOfficerAssignmentException;
import org.apache.fineract.portfolio.savings.exception.SavingsOfficerUnassignmentException;
import org.apache.fineract.portfolio.savings.exception.TransactionUpdateNotAllowedException;
import org.apache.fineract.portfolio.transfer.api.TransferApiConstants;
import org.apache.fineract.useradministration.domain.AppUser;
import org.apache.fineract.useradministration.domain.AppUserRepositoryWrapper;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
@Slf4j
@RequiredArgsConstructor
public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements SavingsAccountWritePlatformService {
private final PlatformSecurityContext context;
private final SavingsAccountDataValidator fromApiJsonDeserializer;
private final SavingsAccountRepositoryWrapper savingAccountRepositoryWrapper;
private final StaffRepositoryWrapper staffRepository;
private final SavingsAccountTransactionRepository savingsAccountTransactionRepository;
private final SavingsAccountAssembler savingAccountAssembler;
private final SavingsAccountTransactionDataValidator savingsAccountTransactionDataValidator;
private final SavingsAccountChargeDataValidator savingsAccountChargeDataValidator;
private final PaymentDetailWritePlatformService paymentDetailWritePlatformService;
private final JournalEntryWritePlatformService journalEntryWritePlatformService;
private final SavingsAccountDomainService savingsAccountDomainService;
private final NoteRepository noteRepository;
private final AccountTransfersReadPlatformService accountTransfersReadPlatformService;
private final AccountAssociationsReadPlatformService accountAssociationsReadPlatformService;
private final ChargeRepositoryWrapper chargeRepository;
private final SavingsAccountChargeRepositoryWrapper savingsAccountChargeRepository;
private final HolidayRepositoryWrapper holidayRepository;
private final WorkingDaysRepositoryWrapper workingDaysRepository;
private final ConfigurationDomainService configurationDomainService;
private final DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository;
private final EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService;
private final AppUserRepositoryWrapper appuserRepository;
private final StandingInstructionRepository standingInstructionRepository;
private final BusinessEventNotifierService businessEventNotifierService;
private final GSIMRepositoy gsimRepository;
private final SavingsAccountInterestPostingService savingsAccountInterestPostingService;
private final ErrorHandler errorHandler;
@Transactional
@Override
public CommandProcessingResult gsimActivate(final Long gsimId, final JsonCommand command) {
Long parentSavingId = gsimId;
GroupSavingsIndividualMonitoring parentSavings = gsimRepository.findById(parentSavingId).orElseThrow();
List<SavingsAccount> childSavings = this.savingAccountRepositoryWrapper.findByGsimId(gsimId);
CommandProcessingResult result = null;
int count = 0;
for (SavingsAccount account : childSavings) {
result = activate(account.getId(), command);
if (result != null) {
count++;
if (count == parentSavings.getChildAccountsCount()) {
parentSavings.setSavingsStatus(SavingsAccountStatusType.ACTIVE.getValue());
gsimRepository.save(parentSavings);
}
}
}
return result;
}
@Transactional
@Override
public CommandProcessingResult activate(final Long savingsId, final JsonCommand command) {
final AppUser user = this.context.authenticatedUser();
this.savingsAccountTransactionDataValidator.validateActivation(command);
final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false);
checkClientOrGroupActive(account);
final Set<Long> existingTransactionIds = new HashSet<>();
final Set<Long> existingReversedTransactionIds = new HashSet<>();
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
final Map<String, Object> changes = account.activate(user, command);
entityDatatableChecksWritePlatformService.runTheCheckForProduct(savingsId, EntityTables.SAVINGS.getName(),
StatusEnum.ACTIVATE.getCode().longValue(), EntityTables.SAVINGS.getForeignKeyColumnNameOnDatatable(), account.productId());
if (!changes.isEmpty()) {
final Locale locale = command.extractLocale();
final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale);
processPostActiveActions(account, fmt, existingTransactionIds, existingReversedTransactionIds);
this.savingAccountRepositoryWrapper.saveAndFlush(account);
}
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false);
businessEventNotifierService.notifyPostBusinessEvent(new SavingsActivateBusinessEvent(account));
return new CommandProcessingResultBuilder() //
.withEntityId(savingsId) //
.withOfficeId(account.officeId()) //
.withClientId(account.clientId()) //
.withGroupId(account.groupId()) //
.withSavingsId(savingsId) //
.with(changes) //
.build();
}
@Override
public void processPostActiveActions(final SavingsAccount account, final DateTimeFormatter fmt, final Set<Long> existingTransactionIds,
final Set<Long> existingReversedTransactionIds) {
getAppUserIfPresent();
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
.isSavingsInterestPostingAtCurrentPeriodEnd();
final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth();
Money amountForDeposit = account.activateWithBalance();
boolean isRegularTransaction = false;
if (amountForDeposit.isGreaterThanZero()) {
boolean isAccountTransfer = false;
this.savingsAccountDomainService.handleDeposit(account, fmt, account.getActivationDate(), amountForDeposit.getAmount(), null,
isAccountTransfer, isRegularTransaction, false);
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
}
account.processAccountUponActivation(isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth);
List<DepositAccountOnHoldTransaction> depositAccountOnHoldTransactions = null;
if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) {
depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository
.findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account);
}
account.validateAccountBalanceDoesNotBecomeNegative(SavingsAccountTransactionType.PAY_CHARGE.name(),
depositAccountOnHoldTransactions, false);
}
@Transactional
@Override
public CommandProcessingResult gsimDeposit(final Long gsimId, final JsonCommand command) {
JsonArray savingsArray = command.arrayOfParameterNamed("savingsArray");
CommandProcessingResult result = null;
for (JsonElement element : savingsArray) {
result = deposit(element.getAsJsonObject().get("childAccountId").getAsLong(),
JsonCommand.fromExistingCommand(command, element));
}
return result;
}
@Transactional
@Override
public CommandProcessingResult deposit(final Long savingsId, final JsonCommand command) {
this.context.authenticatedUser();
this.savingsAccountTransactionDataValidator.validate(command);
boolean isGsim = false;
final boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus();
final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, backdatedTxnsAllowedTill);
if (account.getGsim() != null) {
isGsim = true;
log.debug("is gsim");
}
checkClientOrGroupActive(account);
final Locale locale = command.extractLocale();
final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale);
final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount");
this.savingsAccountTransactionDataValidator.validateTransactionWithPivotDate(transactionDate, account);
final Map<String, Object> changes = new LinkedHashMap<>();
final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes);
boolean isAccountTransfer = false;
boolean isRegularTransaction = true;
final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(account, fmt, transactionDate,
transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill);
if (isGsim && (deposit.getId() != null)) {
log.debug("Deposit account has been created: {} ", deposit);
GroupSavingsIndividualMonitoring gsim = gsimRepository.findById(account.getGsim().getId()).orElseThrow();
log.debug("parent deposit : {} ", gsim.getParentDeposit());
log.debug("child account : {} ", savingsId);
BigDecimal currentBalance = gsim.getParentDeposit();
BigDecimal newBalance = currentBalance.add(transactionAmount);
gsim.setParentDeposit(newBalance);
gsimRepository.save(gsim);
log.debug("balance after making deposit : {} ",
gsimRepository.findById(account.getGsim().getId()).orElseThrow().getParentDeposit());
}
final String noteText = command.stringValueOfParameterNamed("note");
if (StringUtils.isNotBlank(noteText)) {
final Note note = Note.savingsTransactionNote(account, deposit, noteText);
this.noteRepository.save(note);
}
return new CommandProcessingResultBuilder() //
.withEntityId(deposit.getId()) //
.withOfficeId(account.officeId()) //
.withClientId(account.clientId()) //
.withGroupId(account.groupId()) //
.withSavingsId(savingsId) //
.with(changes) //
.build();
}
private Long saveTransactionToGenerateTransactionId(final SavingsAccountTransaction transaction) {
this.savingsAccountTransactionRepository.saveAndFlush(transaction);
return transaction.getId();
}
@Transactional
@Override
public CommandProcessingResult withdrawal(final Long savingsId, final JsonCommand command) {
this.savingsAccountTransactionDataValidator.validate(command);
boolean isGsim = false;
final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount");
final Locale locale = command.extractLocale();
final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale);
final Map<String, Object> changes = new LinkedHashMap<>();
final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes);
final boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus();
final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, backdatedTxnsAllowedTill);
if (account.getGsim() != null) {
isGsim = true;
}
checkClientOrGroupActive(account);
this.savingsAccountTransactionDataValidator.validateTransactionWithPivotDate(transactionDate, account);
final boolean isAccountTransfer = false;
final boolean isRegularTransaction = true;
final boolean isApplyWithdrawFee = true;
final boolean isInterestTransfer = false;
final boolean isWithdrawBalance = false;
final SavingsTransactionBooleanValues transactionBooleanValues = new SavingsTransactionBooleanValues(isAccountTransfer,
isRegularTransaction, isApplyWithdrawFee, isInterestTransfer, isWithdrawBalance);
final SavingsAccountTransaction withdrawal = this.savingsAccountDomainService.handleWithdrawal(account, fmt, transactionDate,
transactionAmount, paymentDetail, transactionBooleanValues, backdatedTxnsAllowedTill);
if (isGsim && (withdrawal.getId() != null)) {
GroupSavingsIndividualMonitoring gsim = gsimRepository.findById(account.getGsim().getId()).orElseThrow();
BigDecimal currentBalance = gsim.getParentDeposit().subtract(transactionAmount);
gsim.setParentDeposit(currentBalance);
gsimRepository.save(gsim);
}
final String noteText = command.stringValueOfParameterNamed("note");
if (StringUtils.isNotBlank(noteText)) {
final Note note = Note.savingsTransactionNote(account, withdrawal, noteText);
this.noteRepository.save(note);
}
return new CommandProcessingResultBuilder() //
.withEntityId(withdrawal.getId()) //
.withOfficeId(account.officeId()) //
.withClientId(account.clientId()) //
.withGroupId(account.groupId()) //
.withSavingsId(savingsId) //
.with(changes)//
.build();
}
@Transactional
@Override
public CommandProcessingResult applyAnnualFee(final Long savingsAccountChargeId, final Long accountId) {
getAppUserIfPresent();
final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository
.findOneWithNotFoundDetection(savingsAccountChargeId, accountId);
final LocalDate currentDate = DateUtils.getBusinessLocalDate();
final DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd MM yyyy").withZone(DateUtils.getDateTimeZoneOfTenant());
while (DateUtils.isBefore(savingsAccountCharge.getDueDate(), currentDate)) {
this.payCharge(savingsAccountCharge, savingsAccountCharge.getDueDate(), savingsAccountCharge.amount(), fmt, false);
}
return new CommandProcessingResultBuilder() //
.withEntityId(savingsAccountCharge.getId()) //
.withOfficeId(savingsAccountCharge.savingsAccount().officeId()) //
.withClientId(savingsAccountCharge.savingsAccount().clientId()) //
.withGroupId(savingsAccountCharge.savingsAccount().groupId()) //
.withSavingsId(savingsAccountCharge.savingsAccount().getId()) //
.build();
}
@Transactional
@Override
public CommandProcessingResult calculateInterest(final Long savingsId) {
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
.isSavingsInterestPostingAtCurrentPeriodEnd();
final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth();
final boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus();
final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, backdatedTxnsAllowedTill);
checkClientOrGroupActive(account);
final LocalDate today = DateUtils.getBusinessLocalDate();
final MathContext mc = new MathContext(15, MoneyHelper.getRoundingMode());
boolean isInterestTransfer = false;
final LocalDate postInterestOnDate = null;
boolean postReversals = false;
account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd,
financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals);
if (backdatedTxnsAllowedTill) {
this.savingsAccountTransactionRepository.saveAll(account.getSavingsAccountTransactionsWithPivotConfig());
}
this.savingAccountRepositoryWrapper.save(account);
return new CommandProcessingResultBuilder() //
.withEntityId(savingsId) //
.withOfficeId(account.officeId()) //
.withClientId(account.clientId()) //
.withGroupId(account.groupId()) //
.withSavingsId(savingsId) //
.build();
}
@Override
@Transactional
public CommandProcessingResult postInterest(final JsonCommand command) {
Long savingsId = command.getSavingsId();
final boolean postInterestAs = command.booleanPrimitiveValueOfParameterNamed("isPostInterestAsOn");
final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
final boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus();
final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, backdatedTxnsAllowedTill);
checkClientOrGroupActive(account);
this.savingsAccountTransactionDataValidator.validateTransactionWithPivotDate(transactionDate, account);
if (postInterestAs) {
if (transactionDate == null) {
throw new PostInterestAsOnDateException(PostInterestAsOnExceptionType.VALID_DATE);
}
if (DateUtils.isBefore(transactionDate, account.accountSubmittedOrActivationDate())) {
throw new PostInterestAsOnDateException(PostInterestAsOnExceptionType.ACTIVATION_DATE);
}
List<SavingsAccountTransaction> savingTransactions = null;
if (backdatedTxnsAllowedTill) {
savingTransactions = account.getSavingsAccountTransactionsWithPivotConfig();
} else {
savingTransactions = account.getTransactions();
}
for (SavingsAccountTransaction savingTransaction : savingTransactions) {
if (DateUtils.isBefore(transactionDate, savingTransaction.getDateOf())) {
throw new PostInterestAsOnDateException(PostInterestAsOnExceptionType.LAST_TRANSACTION_DATE);
}
}
if (DateUtils.isDateInTheFuture(transactionDate)) {
throw new PostInterestAsOnDateException(PostInterestAsOnExceptionType.FUTURE_DATE);
}
}
postInterest(account, postInterestAs, transactionDate, backdatedTxnsAllowedTill);
businessEventNotifierService.notifyPostBusinessEvent(new SavingsPostInterestBusinessEvent(account));
return new CommandProcessingResultBuilder() //
.withEntityId(savingsId) //
.withOfficeId(account.officeId()) //
.withClientId(account.clientId()) //
.withGroupId(account.groupId()) //
.withSavingsId(savingsId) //
.build();
}
@Transactional
@Override
public void postInterest(final SavingsAccount account, final boolean postInterestAs, final LocalDate transactionDate,
final boolean backdatedTxnsAllowedTill) {
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
.isSavingsInterestPostingAtCurrentPeriodEnd();
final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth();
if (account.getNominalAnnualInterestRate().compareTo(BigDecimal.ZERO) > 0
|| (account.allowOverdraft() && account.getNominalAnnualInterestRateOverdraft().compareTo(BigDecimal.ZERO) > 0)) {
final Set<Long> existingTransactionIds = new HashSet<>();
final Set<Long> existingReversedTransactionIds = new HashSet<>();
if (backdatedTxnsAllowedTill) {
updateSavingsTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
} else {
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
}
final LocalDate today = DateUtils.getBusinessLocalDate();
final MathContext mc = new MathContext(10, MoneyHelper.getRoundingMode());
boolean isInterestTransfer = false;
LocalDate postInterestOnDate = null;
if (postInterestAs) {
postInterestOnDate = transactionDate;
}
boolean postReversals = false;
account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth,
postInterestOnDate, backdatedTxnsAllowedTill, postReversals);
if (!backdatedTxnsAllowedTill) {
List<SavingsAccountTransaction> transactions = account.getTransactions();
for (SavingsAccountTransaction accountTransaction : transactions) {
if (accountTransaction.getId() == null) {
this.savingsAccountTransactionRepository.save(accountTransaction);
}
}
}
if (backdatedTxnsAllowedTill) {
this.savingsAccountTransactionRepository.saveAll(account.getSavingsAccountTransactionsWithPivotConfig());
}
this.savingAccountRepositoryWrapper.saveAndFlush(account);
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, backdatedTxnsAllowedTill);
}
}
@Transactional
@Override
@Retry(name = "postInterest", fallbackMethod = "fallbackPostInterest")
public SavingsAccountData postInterest(SavingsAccountData savingsAccountData, final boolean postInterestAs,
final LocalDate transactionDate, final boolean backdatedTxnsAllowedTill) {
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
.isSavingsInterestPostingAtCurrentPeriodEnd();
final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth();
if (MathUtil.isGreaterThanZero(savingsAccountData.getNominalAnnualInterestRate()) || (savingsAccountData.isAllowOverdraft()
&& MathUtil.isGreaterThanZero(savingsAccountData.getNominalAnnualInterestRateOverdraft()))) {
final Set<Long> existingTransactionIds = new HashSet<>();
final Set<Long> existingReversedTransactionIds = new HashSet<>();
updateExistingTransactionsDetails(savingsAccountData, existingTransactionIds, existingReversedTransactionIds);
final LocalDate today = DateUtils.getBusinessLocalDate();
final MathContext mc = new MathContext(10, MoneyHelper.getRoundingMode());
boolean isInterestTransfer = false;
LocalDate postInterestOnDate = null;
if (postInterestAs) {
postInterestOnDate = transactionDate;
}
savingsAccountData = this.savingsAccountInterestPostingService.postInterest(mc, today, isInterestTransfer,
isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill,
savingsAccountData);
if (!backdatedTxnsAllowedTill) {
List<SavingsAccountTransactionData> transactions = savingsAccountData.getSavingsAccountTransactionData();
for (SavingsAccountTransactionData accountTransaction : transactions) {
if (accountTransaction.getId() == null) {
savingsAccountData.setNewSavingsAccountTransactionData(accountTransaction);
}
}
}
savingsAccountData.setExistingTransactionIds(existingTransactionIds);
savingsAccountData.setExistingReversedTransactionIds(existingReversedTransactionIds);
}
return savingsAccountData;
}
@Override
public CommandProcessingResult reverseTransaction(final Long savingsId, final Long transactionId,
final boolean allowAccountTransferModification, final JsonCommand command) {
final boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus();
final boolean isBulk = command.booleanPrimitiveValueOfParameterNamed("isBulk");
final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, backdatedTxnsAllowedTill);
final SavingsAccountTransaction savingsAccountTransaction = this.savingsAccountTransactionRepository
.findOneByIdAndSavingsAccountId(transactionId, savingsId);
if (savingsAccountTransaction == null) {
throw new SavingsAccountTransactionNotFoundException(savingsId, transactionId);
}
if (!allowAccountTransferModification
&& this.accountTransfersReadPlatformService.isAccountTransfer(transactionId, PortfolioAccountType.SAVINGS)) {
throw new PlatformServiceUnavailableException("error.msg.saving.account.transfer.transaction.update.not.allowed",
"Savings account transaction:" + transactionId + " update not allowed as it involves in account transfer",
transactionId);
}
if (!account.allowModify()) {
throw new PlatformServiceUnavailableException("error.msg.saving.account.transaction.update.not.allowed",
"Savings account transaction:" + transactionId + " update not allowed for this savings type", transactionId);
}
if (account.isNotActive()) {
throwValidationForActiveStatus(SavingsApiConstants.undoTransactionAction);
}
SavingsAccountTransaction reversal = null;
Long reversalId = null;
checkClientOrGroupActive(account);
List<SavingsAccountTransaction> savingsAccountTransactions = null;
if (isBulk) {
String transactionRefNo = savingsAccountTransaction.getRefNo();
savingsAccountTransactions = this.savingsAccountTransactionRepository.findByRefNo(transactionRefNo);
reversal = this.savingsAccountDomainService.handleReversal(account, savingsAccountTransactions, backdatedTxnsAllowedTill);
} else {
reversal = this.savingsAccountDomainService.handleReversal(account, Collections.singletonList(savingsAccountTransaction),
backdatedTxnsAllowedTill);
}
reversalId = reversal.getId();
return new CommandProcessingResultBuilder() //
.withEntityId(reversalId) //
.withOfficeId(account.officeId()) //
.withClientId(account.clientId()) //
.withGroupId(account.groupId()) //
.withSavingsId(savingsId) //
.build();
}
@Override
public CommandProcessingResult undoTransaction(final Long savingsId, final Long transactionId,
final boolean allowAccountTransferModification) {
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
.isSavingsInterestPostingAtCurrentPeriodEnd();
final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth();
final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false);
final Set<Long> existingTransactionIds = new HashSet<>();
final Set<Long> existingReversedTransactionIds = new HashSet<>();
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
final SavingsAccountTransaction savingsAccountTransaction = this.savingsAccountTransactionRepository
.findOneByIdAndSavingsAccountId(transactionId, savingsId);
if (savingsAccountTransaction == null) {
throw new SavingsAccountTransactionNotFoundException(savingsId, transactionId);
}
this.savingsAccountTransactionDataValidator.validateTransactionWithPivotDate(savingsAccountTransaction.getTransactionDate(),
account);
if (!allowAccountTransferModification
&& this.accountTransfersReadPlatformService.isAccountTransfer(transactionId, PortfolioAccountType.SAVINGS)) {
throw new PlatformServiceUnavailableException("error.msg.saving.account.transfer.transaction.update.not.allowed",
"Savings account transaction:" + transactionId + " update not allowed as it involves in account transfer",
transactionId);
}
if (!account.allowModify()) {
throw new PlatformServiceUnavailableException("error.msg.saving.account.transaction.update.not.allowed",
"Savings account transaction:" + transactionId + " update not allowed for this savings type", transactionId);
}
final LocalDate today = DateUtils.getBusinessLocalDate();
final MathContext mc = new MathContext(15, MoneyHelper.getRoundingMode());
if (account.isNotActive()) {
throwValidationForActiveStatus(SavingsApiConstants.undoTransactionAction);
}
account.undoTransaction(transactionId);
// undoing transaction is withdrawal then undo withdrawal fee
// transaction if any
if (savingsAccountTransaction.isWithdrawal()) {
final SavingsAccountTransaction nextSavingsAccountTransaction = this.savingsAccountTransactionRepository
.findOneByIdAndSavingsAccountId(transactionId + 1, savingsId);
if (nextSavingsAccountTransaction != null && nextSavingsAccountTransaction.isWithdrawalFeeAndNotReversed()) {
account.undoTransaction(transactionId + 1);
}
}
boolean isInterestTransfer = false;
LocalDate postInterestOnDate = null;
boolean postReversals = false;
checkClientOrGroupActive(account);
if (savingsAccountTransaction.isPostInterestCalculationRequired()
&& account.isBeforeLastPostingPeriod(savingsAccountTransaction.getTransactionDate(), false)) {
account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth,
postInterestOnDate, false, postReversals);
} else {
account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd,
financialYearBeginningMonth, postInterestOnDate, false, postReversals);
}
List<DepositAccountOnHoldTransaction> depositAccountOnHoldTransactions = null;
if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) {
depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository
.findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account);
}
account.validateAccountBalanceDoesNotBecomeNegative(SavingsApiConstants.undoTransactionAction, depositAccountOnHoldTransactions,
false);
account.activateAccountBasedOnBalance();
this.savingAccountRepositoryWrapper.saveAndFlush(account);
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false);
return new CommandProcessingResultBuilder() //
.withEntityId(savingsId) //
.withOfficeId(account.officeId()) //
.withClientId(account.clientId()) //
.withGroupId(account.groupId()) //
.withSavingsId(savingsId) //
.build();
}
@Override
public CommandProcessingResult adjustSavingsTransaction(final Long savingsId, final Long transactionId, final JsonCommand command) {
context.authenticatedUser();
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
.isSavingsInterestPostingAtCurrentPeriodEnd();
final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth();
final Long relaxingDaysConfigForPivotDate = this.configurationDomainService.retrieveRelaxingDaysConfigForPivotDate();
final SavingsAccountTransaction savingsAccountTransaction = this.savingsAccountTransactionRepository
.findOneByIdAndSavingsAccountId(transactionId, savingsId);
if (savingsAccountTransaction == null) {
throw new SavingsAccountTransactionNotFoundException(savingsId, transactionId);
}
if (!(savingsAccountTransaction.isDeposit() || savingsAccountTransaction.isWithdrawal())
|| savingsAccountTransaction.isReversed()) {
throw new TransactionUpdateNotAllowedException(savingsId, transactionId);
}
if (this.accountTransfersReadPlatformService.isAccountTransfer(transactionId, PortfolioAccountType.SAVINGS)) {
throw new PlatformServiceUnavailableException("error.msg.saving.account.transfer.transaction.update.not.allowed",
"Savings account transaction:" + transactionId + " update not allowed as it involves in account transfer",
transactionId);
}
this.savingsAccountTransactionDataValidator.validate(command);
final LocalDate today = DateUtils.getBusinessLocalDate();
final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false);
if (account.isNotActive()) {
throwValidationForActiveStatus(SavingsApiConstants.adjustTransactionAction);
}
if (!account.allowModify()) {
throw new PlatformServiceUnavailableException("error.msg.saving.account.transaction.update.not.allowed",
"Savings account transaction:" + transactionId + " update not allowed for this savings type", transactionId);
}
final Set<Long> existingTransactionIds = new HashSet<>();
final Set<Long> existingReversedTransactionIds = new HashSet<>();
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
final Locale locale = command.extractLocale();
final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale);
final LocalDate transactionDate = command.localDateValueOfParameterNamed(SavingsApiConstants.transactionDateParamName);
final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed(SavingsApiConstants.transactionAmountParamName);
final Map<String, Object> changes = new LinkedHashMap<>();
final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes);
final MathContext mc = new MathContext(10, MoneyHelper.getRoundingMode());
account.undoTransaction(transactionId);
// for undo withdrawal fee
final SavingsAccountTransaction nextSavingsAccountTransaction = this.savingsAccountTransactionRepository
.findOneByIdAndSavingsAccountId(transactionId + 1, savingsId);
if (nextSavingsAccountTransaction != null && nextSavingsAccountTransaction.isWithdrawalFeeAndNotReversed()) {
account.undoTransaction(transactionId + 1);
}
SavingsAccountTransaction transaction = null;
boolean isInterestTransfer = false;
Integer accountType = null;
final SavingsAccountTransactionDTO transactionDTO = new SavingsAccountTransactionDTO(fmt, transactionDate, transactionAmount,
paymentDetail, null, accountType);
UUID refNo = UUID.randomUUID();
if (savingsAccountTransaction.isDeposit()) {
transaction = account.deposit(transactionDTO, false, relaxingDaysConfigForPivotDate, refNo.toString());
} else {
transaction = account.withdraw(transactionDTO, true, false, relaxingDaysConfigForPivotDate, refNo.toString());
}
final Long newtransactionId = saveTransactionToGenerateTransactionId(transaction);
final LocalDate postInterestOnDate = null;
boolean postReversals = false;
if (account.isBeforeLastPostingPeriod(transactionDate, false)
|| account.isBeforeLastPostingPeriod(savingsAccountTransaction.getTransactionDate(), false)) {
account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth,
postInterestOnDate, false, postReversals);
} else {
account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd,
financialYearBeginningMonth, postInterestOnDate, false, postReversals);
}
List<DepositAccountOnHoldTransaction> depositAccountOnHoldTransactions = null;
if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) {
depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository
.findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account);
}
account.validateAccountBalanceDoesNotBecomeNegative(SavingsApiConstants.adjustTransactionAction, depositAccountOnHoldTransactions,
false);
account.activateAccountBasedOnBalance();
this.savingAccountRepositoryWrapper.saveAndFlush(account);
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false);
return new CommandProcessingResultBuilder() //
.withEntityId(newtransactionId) //
.withOfficeId(account.officeId()) //
.withClientId(account.clientId()) //
.withGroupId(account.groupId()) //
.withSavingsId(savingsId) //
.with(changes)//
.build();
}
/**
*
*/
private void throwValidationForActiveStatus(final String actionName) {
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(SAVINGS_ACCOUNT_RESOURCE_NAME + actionName);
baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("account.is.not.active");
throw new PlatformApiDataValidationException(dataValidationErrors);
}
private void checkClientOrGroupActive(final SavingsAccount account) {
final Client client = account.getClient();
if (client != null) {
if (client.isNotActive()) {
throw new ClientNotActiveException(client.getId());
}
}
final Group group = account.group();
if (group != null) {
if (group.isNotActive()) {
throw new GroupNotActiveException(group.getId());
}
}
}
@Override
public CommandProcessingResult bulkGSIMClose(final Long gsimId, final JsonCommand command) {
final Long parentSavingId = gsimId;
GroupSavingsIndividualMonitoring parentSavings = gsimRepository.findById(parentSavingId).orElseThrow();
List<SavingsAccount> childSavings = this.savingAccountRepositoryWrapper.findByGsimId(gsimId);
CommandProcessingResult result = null;
int count = 0;
for (SavingsAccount account : childSavings) {
result = close(account.getId(), command);
if (result != null) {
count++;
if (count == parentSavings.getChildAccountsCount()) {
parentSavings.setSavingsStatus(SavingsAccountStatusType.CLOSED.getValue());
gsimRepository.save(parentSavings);
}
}
}
return result;
}
@Override
public CommandProcessingResult close(final Long savingsId, final JsonCommand command) {
final AppUser user = this.context.authenticatedUser();
final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false);
this.savingsAccountTransactionDataValidator.validateClosing(command, account);
final boolean isLinkedWithAnyActiveLoan = this.accountAssociationsReadPlatformService.isLinkedWithAnyActiveAccount(savingsId);
if (isLinkedWithAnyActiveLoan) {
final String defaultUserMessage = "Closing savings account with id:" + savingsId
+ " is not allowed, since it is linked with one of the active accounts";
throw new SavingsAccountClosingNotAllowedException("linked", defaultUserMessage, savingsId);
}
entityDatatableChecksWritePlatformService.runTheCheckForProduct(savingsId, EntityTables.SAVINGS.getName(),
StatusEnum.CLOSE.getCode().longValue(), EntityTables.SAVINGS.getForeignKeyColumnNameOnDatatable(), account.productId());
final boolean isWithdrawBalance = command.booleanPrimitiveValueOfParameterNamed(withdrawBalanceParamName);
final Locale locale = command.extractLocale();
final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale);
final LocalDate closedDate = command.localDateValueOfParameterNamed(SavingsApiConstants.closedOnDateParamName);
final boolean isPostInterest = command.booleanPrimitiveValueOfParameterNamed(SavingsApiConstants.postInterestValidationOnClosure);
// postInterest(account,closedDate,flag);
if (isPostInterest) {
boolean postInterestOnClosingDate = false;
List<SavingsAccountTransaction> savingTransactions = account.getTransactions();
for (SavingsAccountTransaction savingTransaction : savingTransactions) {
if (savingTransaction.isInterestPosting() && savingTransaction.isNotReversed()
&& DateUtils.isEqual(closedDate, savingTransaction.getTransactionDate())) {
postInterestOnClosingDate = true;
break;
}
}
if (!postInterestOnClosingDate) {
throw new PostInterestClosingDateException();
}
}
final Map<String, Object> changes = new LinkedHashMap<>();
if (isWithdrawBalance && account.getSummary().getAccountBalance(account.getCurrency()).isGreaterThanZero()) {
final BigDecimal transactionAmount = account.getSummary().getAccountBalance();
final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes);
final boolean isAccountTransfer = false;
final boolean isRegularTransaction = true;
final boolean isApplyWithdrawFee = false;
final boolean isInterestTransfer = false;
final SavingsTransactionBooleanValues transactionBooleanValues = new SavingsTransactionBooleanValues(isAccountTransfer,
isRegularTransaction, isApplyWithdrawFee, isInterestTransfer, isWithdrawBalance);
this.savingsAccountDomainService.handleWithdrawal(account, fmt, closedDate, transactionAmount, paymentDetail,
transactionBooleanValues, false);
}
final Map<String, Object> accountChanges = account.close(user, command);
changes.putAll(accountChanges);
if (!changes.isEmpty()) {
this.savingAccountRepositoryWrapper.save(account);
final String noteText = command.stringValueOfParameterNamed("note");
if (StringUtils.isNotBlank(noteText)) {
final Note note = Note.savingNote(account, noteText);
changes.put("note", noteText);
this.noteRepository.save(note);
}
}
businessEventNotifierService.notifyPostBusinessEvent(new SavingsCloseBusinessEvent(account));
// disable all standing orders linked to the savings account
disableStandingInstructionsLinkedToClosedSavings(account);
return new CommandProcessingResultBuilder() //
.withEntityId(savingsId) //
.withOfficeId(account.officeId()) //
.withClientId(account.clientId()) //
.withGroupId(account.groupId()) //
.withSavingsId(savingsId) //
.with(changes) //
.build();
}
@Override
public SavingsAccountTransaction initiateSavingsTransfer(final SavingsAccount savingsAccount, final LocalDate transferDate) {
getAppUserIfPresent();
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
.isSavingsInterestPostingAtCurrentPeriodEnd();
validateTransactionsForTransfer(savingsAccount, transferDate);
final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth();
this.savingAccountAssembler.setHelpers(savingsAccount);
final Set<Long> existingTransactionIds = new HashSet<>();
final Set<Long> existingReversedTransactionIds = new HashSet<>();
updateExistingTransactionsDetails(savingsAccount, existingTransactionIds, existingReversedTransactionIds);
final SavingsAccountTransaction newTransferTransaction = SavingsAccountTransaction.initiateTransfer(savingsAccount,
savingsAccount.office(), transferDate);
savingsAccount.addTransaction(newTransferTransaction);
savingsAccount.setStatus(SavingsAccountStatusType.TRANSFER_IN_PROGRESS.getValue());
final MathContext mc = MathContext.DECIMAL64;
boolean isInterestTransfer = false;
LocalDate postInterestOnDate = null;
boolean postReversals = false;
savingsAccount.calculateInterestUsing(mc, transferDate, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd,
financialYearBeginningMonth, postInterestOnDate, false, postReversals);
this.savingsAccountTransactionRepository.save(newTransferTransaction);
this.savingAccountRepositoryWrapper.saveAndFlush(savingsAccount);
postJournalEntries(savingsAccount, existingTransactionIds, existingReversedTransactionIds, false);
return newTransferTransaction;
}
@Override
public SavingsAccountTransaction withdrawSavingsTransfer(final SavingsAccount savingsAccount, final LocalDate transferDate) {
getAppUserIfPresent();
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
.isSavingsInterestPostingAtCurrentPeriodEnd();
final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth();
this.savingAccountAssembler.setHelpers(savingsAccount);
final Set<Long> existingTransactionIds = new HashSet<>();
final Set<Long> existingReversedTransactionIds = new HashSet<>();
updateExistingTransactionsDetails(savingsAccount, existingTransactionIds, existingReversedTransactionIds);
final SavingsAccountTransaction withdrawtransferTransaction = SavingsAccountTransaction.withdrawTransfer(savingsAccount,
savingsAccount.office(), transferDate);
savingsAccount.addTransaction(withdrawtransferTransaction);
savingsAccount.setStatus(SavingsAccountStatusType.ACTIVE.getValue());
final MathContext mc = MathContext.DECIMAL64;
boolean isInterestTransfer = false;
LocalDate postInterestOnDate = null;
boolean postReversals = false;
savingsAccount.calculateInterestUsing(mc, transferDate, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd,
financialYearBeginningMonth, postInterestOnDate, false, postReversals);
this.savingsAccountTransactionRepository.save(withdrawtransferTransaction);
this.savingAccountRepositoryWrapper.saveAndFlush(savingsAccount);
postJournalEntries(savingsAccount, existingTransactionIds, existingReversedTransactionIds, false);
return withdrawtransferTransaction;
}
@Override
public void rejectSavingsTransfer(final SavingsAccount savingsAccount) {
this.savingAccountAssembler.setHelpers(savingsAccount);
savingsAccount.setStatus(SavingsAccountStatusType.TRANSFER_ON_HOLD.getValue());
this.savingAccountRepositoryWrapper.save(savingsAccount);
}
@Override
public SavingsAccountTransaction acceptSavingsTransfer(final SavingsAccount savingsAccount, final LocalDate transferDate,
final Office acceptedInOffice, final Staff fieldOfficer) {
getAppUserIfPresent();
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
.isSavingsInterestPostingAtCurrentPeriodEnd();
final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth();
this.savingAccountAssembler.setHelpers(savingsAccount);
final Set<Long> existingTransactionIds = new HashSet<>();
final Set<Long> existingReversedTransactionIds = new HashSet<>();
updateExistingTransactionsDetails(savingsAccount, existingTransactionIds, existingReversedTransactionIds);
final SavingsAccountTransaction acceptTransferTransaction = SavingsAccountTransaction.approveTransfer(savingsAccount,
acceptedInOffice, transferDate);
savingsAccount.addTransaction(acceptTransferTransaction);
savingsAccount.setStatus(SavingsAccountStatusType.ACTIVE.getValue());
if (fieldOfficer != null) {
savingsAccount.reassignSavingsOfficer(fieldOfficer, transferDate);
}
boolean isInterestTransfer = false;
final MathContext mc = MathContext.DECIMAL64;
LocalDate postInterestOnDate = null;
boolean postReversals = false;
savingsAccount.calculateInterestUsing(mc, transferDate, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd,
financialYearBeginningMonth, postInterestOnDate, false, postReversals);
this.savingsAccountTransactionRepository.save(acceptTransferTransaction);
this.savingAccountRepositoryWrapper.saveAndFlush(savingsAccount);
postJournalEntries(savingsAccount, existingTransactionIds, existingReversedTransactionIds, false);
return acceptTransferTransaction;
}
@Transactional
@Override
public CommandProcessingResult addSavingsAccountCharge(final JsonCommand command) {
this.context.authenticatedUser();
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(SAVINGS_ACCOUNT_RESOURCE_NAME);
final Long savingsAccountId = command.getSavingsId();
this.savingsAccountChargeDataValidator.validateAdd(command.json());
final SavingsAccount savingsAccount = this.savingAccountAssembler.assembleFrom(savingsAccountId, false);
checkClientOrGroupActive(savingsAccount);
final Locale locale = command.extractLocale();
final String format = command.dateFormat();
final DateTimeFormatter fmt = StringUtils.isNotBlank(format) ? DateTimeFormatter.ofPattern(format).withLocale(locale)
: DateTimeFormatter.ofPattern("dd MM yyyy");
final Long chargeDefinitionId = command.longValueOfParameterNamed(chargeIdParamName);
final Charge chargeDefinition = this.chargeRepository.findOneWithNotFoundDetection(chargeDefinitionId);
Integer chargeTimeType = chargeDefinition.getChargeTimeType();
LocalDate dueAsOfDateParam = command.localDateValueOfParameterNamed(dueAsOfDateParamName);
if ((chargeTimeType.equals(ChargeTimeType.WITHDRAWAL_FEE.getValue())
|| chargeTimeType.equals(ChargeTimeType.SAVINGS_NOACTIVITY_FEE.getValue())) && dueAsOfDateParam != null) {
baseDataValidator.reset().parameter(dueAsOfDateParamName).value(dueAsOfDateParam.format(fmt))
.failWithCodeNoParameterAddedToErrorCode(
"charge.due.date.is.invalid.for." + ChargeTimeType.fromInt(chargeTimeType).getCode());
}
final SavingsAccountCharge savingsAccountCharge = SavingsAccountCharge.createNewFromJson(savingsAccount, chargeDefinition, command);
if (chargeDefinition.isEnableFreeWithdrawal()) {
savingsAccountCharge.setFreeWithdrawalCount(0);
}
if (savingsAccountCharge.getDueDate() != null) {
// transaction date should not be on a holiday or non working day
if (!this.configurationDomainService.allowTransactionsOnHolidayEnabled()
&& this.holidayRepository.isHoliday(savingsAccount.officeId(), savingsAccountCharge.getDueDate())) {
baseDataValidator.reset().parameter(dueAsOfDateParamName).value(savingsAccountCharge.getDueDate().format(fmt))
.failWithCodeNoParameterAddedToErrorCode("charge.due.date.is.on.holiday");
}
if (!this.configurationDomainService.allowTransactionsOnNonWorkingDayEnabled()
&& !this.workingDaysRepository.isWorkingDay(savingsAccountCharge.getDueDate())) {
baseDataValidator.reset().parameter(dueAsOfDateParamName).value(savingsAccountCharge.getDueDate().format(fmt))
.failWithCodeNoParameterAddedToErrorCode("charge.due.date.is.a.nonworking.day");
}
}
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
}
savingsAccount.addCharge(fmt, savingsAccountCharge, chargeDefinition);
this.savingsAccountChargeRepository.save(savingsAccountCharge);
this.savingAccountRepositoryWrapper.saveAndFlush(savingsAccount);
return new CommandProcessingResultBuilder() //
.withEntityId(savingsAccountCharge.getId()) //
.withOfficeId(savingsAccount.officeId()) //
.withClientId(savingsAccount.clientId()) //
.withGroupId(savingsAccount.groupId()) //
.withSavingsId(savingsAccountId) //
.build();
}
@Transactional
@Override
public CommandProcessingResult updateSavingsAccountCharge(final JsonCommand command) {
this.context.authenticatedUser();
this.savingsAccountChargeDataValidator.validateUpdate(command.json());
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(SAVINGS_ACCOUNT_RESOURCE_NAME);
final Long savingsAccountId = command.getSavingsId();
// SavingsAccount Charge entity
final Long savingsChargeId = command.entityId();
final SavingsAccount savingsAccount = this.savingAccountAssembler.assembleFrom(savingsAccountId, false);
checkClientOrGroupActive(savingsAccount);
final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository.findOneWithNotFoundDetection(savingsChargeId,
savingsAccountId);
final Map<String, Object> changes = savingsAccountCharge.update(command);
if (savingsAccountCharge.getDueDate() != null) {
final Locale locale = command.extractLocale();
final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale);
// transaction date should not be on a holiday or non working day
if (!this.configurationDomainService.allowTransactionsOnHolidayEnabled()
&& this.holidayRepository.isHoliday(savingsAccount.officeId(), savingsAccountCharge.getDueDate())) {
baseDataValidator.reset().parameter(dueAsOfDateParamName).value(savingsAccountCharge.getDueDate().format(fmt))
.failWithCodeNoParameterAddedToErrorCode("charge.due.date.is.on.holiday");
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
}
}
if (!this.configurationDomainService.allowTransactionsOnNonWorkingDayEnabled()
&& !this.workingDaysRepository.isWorkingDay(savingsAccountCharge.getDueDate())) {
baseDataValidator.reset().parameter(dueAsOfDateParamName).value(savingsAccountCharge.getDueDate().format(fmt))
.failWithCodeNoParameterAddedToErrorCode("charge.due.date.is.a.nonworking.day");
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
}
}
}
this.savingsAccountChargeRepository.saveAndFlush(savingsAccountCharge);
return new CommandProcessingResultBuilder() //
.withEntityId(savingsAccountCharge.getId()) //
.withOfficeId(savingsAccountCharge.savingsAccount().officeId()) //
.withClientId(savingsAccountCharge.savingsAccount().clientId()) //
.withGroupId(savingsAccountCharge.savingsAccount().groupId()) //
.withSavingsId(savingsAccountCharge.savingsAccount().getId()) //
.with(changes) //
.build();
}
@Transactional
@Override
public CommandProcessingResult waiveCharge(final Long savingsAccountId, final Long savingsAccountChargeId) {
context.authenticatedUser();
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
.isSavingsInterestPostingAtCurrentPeriodEnd();
final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth();
final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository
.findOneWithNotFoundDetection(savingsAccountChargeId, savingsAccountId);
// Get Savings account from savings charge
final SavingsAccount account = savingsAccountCharge.savingsAccount();
final boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus();
this.savingAccountAssembler.loadTransactionsToSavingsAccount(account, backdatedTxnsAllowedTill);
final Set<Long> existingTransactionIds = new HashSet<>();
final Set<Long> existingReversedTransactionIds = new HashSet<>();
if (backdatedTxnsAllowedTill) {
updateSavingsTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
} else {
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
}
account.waiveCharge(savingsAccountChargeId, backdatedTxnsAllowedTill);
boolean isInterestTransfer = false;
LocalDate postInterestOnDate = null;
final MathContext mc = MathContext.DECIMAL64;
boolean postReversals = false;
if (account.isBeforeLastPostingPeriod(savingsAccountCharge.getDueDate(), backdatedTxnsAllowedTill)) {
final LocalDate today = DateUtils.getBusinessLocalDate();
account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth,
postInterestOnDate, backdatedTxnsAllowedTill, postReversals);
} else {
final LocalDate today = DateUtils.getBusinessLocalDate();
account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd,
financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals);
}
List<DepositAccountOnHoldTransaction> depositAccountOnHoldTransactions = null;
if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) {
depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository
.findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account);
}
account.validateAccountBalanceDoesNotBecomeNegative(SavingsApiConstants.waiveChargeTransactionAction,
depositAccountOnHoldTransactions, backdatedTxnsAllowedTill);
if (backdatedTxnsAllowedTill) {
this.savingsAccountTransactionRepository.saveAll(account.getSavingsAccountTransactionsWithPivotConfig());
}
this.savingAccountRepositoryWrapper.saveAndFlush(account);
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, backdatedTxnsAllowedTill);
return new CommandProcessingResultBuilder() //
.withEntityId(savingsAccountChargeId) //
.withOfficeId(account.officeId()) //
.withClientId(account.clientId()) //
.withGroupId(account.groupId()) //
.withSavingsId(savingsAccountId) //
.build();
}
@Transactional
@Override
public CommandProcessingResult deleteSavingsAccountCharge(final Long savingsAccountId, final Long savingsAccountChargeId,
@SuppressWarnings("unused") final JsonCommand command) {
this.context.authenticatedUser();
final SavingsAccount savingsAccount = this.savingAccountAssembler.assembleFrom(savingsAccountId, false);
checkClientOrGroupActive(savingsAccount);
final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository
.findOneWithNotFoundDetection(savingsAccountChargeId, savingsAccountId);
savingsAccount.removeCharge(savingsAccountCharge);
this.savingAccountRepositoryWrapper.saveAndFlush(savingsAccount);
return new CommandProcessingResultBuilder() //
.withEntityId(savingsAccountChargeId) //
.withOfficeId(savingsAccount.officeId()) //
.withClientId(savingsAccount.clientId()) //
.withGroupId(savingsAccount.groupId()) //
.withSavingsId(savingsAccountId) //
.build();
}
@Override
public CommandProcessingResult payCharge(final Long savingsAccountId, final Long savingsAccountChargeId, final JsonCommand command) {
context.authenticatedUser();
this.savingsAccountChargeDataValidator.validatePayCharge(command.json());
final Locale locale = command.extractLocale();
final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale);
final BigDecimal amountPaid = command.bigDecimalValueOfParameterNamed(amountParamName);
final LocalDate transactionDate = command.localDateValueOfParameterNamed(dueAsOfDateParamName);
final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository
.findOneWithNotFoundDetection(savingsAccountChargeId, savingsAccountId);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(SAVINGS_ACCOUNT_RESOURCE_NAME);
// transaction date should not be on a holiday or non working day
if (!this.configurationDomainService.allowTransactionsOnHolidayEnabled()
&& this.holidayRepository.isHoliday(savingsAccountCharge.savingsAccount().officeId(), transactionDate)) {
baseDataValidator.reset().parameter(dueAsOfDateParamName).value(transactionDate.format(fmt))
.failWithCodeNoParameterAddedToErrorCode("transaction.not.allowed.transaction.date.is.on.holiday");
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
}
}
if (!this.configurationDomainService.allowTransactionsOnNonWorkingDayEnabled()
&& !this.workingDaysRepository.isWorkingDay(transactionDate)) {
baseDataValidator.reset().parameter(dueAsOfDateParamName).value(transactionDate.format(fmt))
.failWithCodeNoParameterAddedToErrorCode("transaction.not.allowed.transaction.date.is.a.nonworking.day");
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
}
}
final boolean backdatedTxnsAllowedTill = false;
SavingsAccountTransaction chargeTransaction = this.payCharge(savingsAccountCharge, transactionDate, amountPaid, fmt,
backdatedTxnsAllowedTill);
final String noteText = command.stringValueOfParameterNamed("note");
if (StringUtils.isNotBlank(noteText)) {
final Note note = Note.savingsTransactionNote(savingsAccountCharge.savingsAccount(), chargeTransaction, noteText);
this.noteRepository.save(note);
}
return new CommandProcessingResultBuilder() //
.withEntityId(savingsAccountCharge.getId()) //
.withOfficeId(savingsAccountCharge.savingsAccount().officeId()) //
.withClientId(savingsAccountCharge.savingsAccount().clientId()) //
.withGroupId(savingsAccountCharge.savingsAccount().groupId()) //
.withSavingsId(savingsAccountCharge.savingsAccount().getId()) //
.build();
}
@Transactional
@Override
public void applyChargeDue(final Long savingsAccountChargeId, final Long accountId) {
// always use current date as transaction date for batch job
final LocalDate transactionDate = DateUtils.getBusinessLocalDate();
final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository
.findOneWithNotFoundDetection(savingsAccountChargeId, accountId);
final DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd MM yyyy").withZone(DateUtils.getDateTimeZoneOfTenant());
while (savingsAccountCharge.isNotFullyPaid() && DateUtils.isBefore(savingsAccountCharge.getDueDate(), transactionDate)) {
payCharge(savingsAccountCharge, transactionDate, savingsAccountCharge.amoutOutstanding(), fmt, false);
}
}
@SuppressWarnings("unused")
public SavingsAccountData fallbackPostInterest(SavingsAccountData savingsAccountData, boolean postInterestAs, LocalDate transactionDate,
boolean backdatedTxnsAllowedTill, Throwable t) {
// NOTE: allow caller to catch the exceptions
// NOTE: wrap throwable only if really necessary
throw errorHandler.getMappable(t, null, null, "savings.postinterest");
}
@Transactional
private SavingsAccountTransaction payCharge(final SavingsAccountCharge savingsAccountCharge, final LocalDate transactionDate,
final BigDecimal amountPaid, final DateTimeFormatter formatter, final boolean backdatedTxnsAllowedTill) {
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
.isSavingsInterestPostingAtCurrentPeriodEnd();
final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth();
// Get Savings account from savings charge
final SavingsAccount account = savingsAccountCharge.savingsAccount();
this.savingAccountAssembler.assignSavingAccountHelpers(account);
final Set<Long> existingTransactionIds = new HashSet<>();
final Set<Long> existingReversedTransactionIds = new HashSet<>();
Pageable sortedByDateAndIdDesc = PageRequest.of(0, 1, Sort.by("dateOf", "id").descending());
List<SavingsAccountTransaction> savingsAccountTransaction = this.savingsAccountTransactionRepository
.findBySavingsAccountIdAndLessThanDateOfAndReversedIsFalse(account.getId(), transactionDate, sortedByDateAndIdDesc);
account.validateAccountBalanceDoesNotViolateOverdraft(savingsAccountTransaction, amountPaid);
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
SavingsAccountTransaction chargeTransaction = account.payCharge(savingsAccountCharge, amountPaid, transactionDate, formatter,
backdatedTxnsAllowedTill, null);
boolean isInterestTransfer = false;
LocalDate postInterestOnDate = null;
final MathContext mc = MathContext.DECIMAL64;
boolean postReversals = false;
if (account.isBeforeLastPostingPeriod(transactionDate, backdatedTxnsAllowedTill)) {
final LocalDate today = DateUtils.getBusinessLocalDate();
account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth,
postInterestOnDate, isInterestTransfer, postReversals);
} else {
final LocalDate today = DateUtils.getBusinessLocalDate();
account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd,
financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals);
}
List<DepositAccountOnHoldTransaction> depositAccountOnHoldTransactions = null;
if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) {
depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository
.findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account);
}
account.validateAccountBalanceDoesNotBecomeNegative("." + SavingsAccountTransactionType.PAY_CHARGE.getCode(),
depositAccountOnHoldTransactions, backdatedTxnsAllowedTill);
saveTransactionToGenerateTransactionId(chargeTransaction);
this.savingAccountRepositoryWrapper.saveAndFlush(account);
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, backdatedTxnsAllowedTill);
return chargeTransaction;
}
private void updateExistingTransactionsDetails(SavingsAccount account, Set<Long> existingTransactionIds,
Set<Long> existingReversedTransactionIds) {
existingTransactionIds.addAll(account.findExistingTransactionIds());
existingReversedTransactionIds.addAll(account.findExistingReversedTransactionIds());
}
private void updateExistingTransactionsDetails(SavingsAccountData account, Set<Long> existingTransactionIds,
Set<Long> existingReversedTransactionIds) {
existingTransactionIds.addAll(account.findCurrentTransactionIdsWithPivotDateConfig());
existingReversedTransactionIds.addAll(account.findCurrentReversedTransactionIdsWithPivotDateConfig());
}
private void updateSavingsTransactionsDetails(SavingsAccount account, Set<Long> existingTransactionIds,
Set<Long> existingReversedTransactionIds) {
existingTransactionIds.addAll(account.findCurrentTransactionIdsWithPivotDateConfig());
existingReversedTransactionIds.addAll(account.findCurrentReversedTransactionIdsWithPivotDateConfig());
}
private void postJournalEntries(final SavingsAccount savingsAccount, final Set<Long> existingTransactionIds,
final Set<Long> existingReversedTransactionIds, final boolean backdatedTxnsAllowedTill) {
boolean isAccountTransfer = false;
final Map<String, Object> accountingBridgeData = savingsAccount.deriveAccountingBridgeData(savingsAccount.getCurrency().getCode(),
existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill);
this.journalEntryWritePlatformService.createJournalEntriesForSavings(accountingBridgeData);
}
@Override
public CommandProcessingResult inactivateCharge(final Long savingsAccountId, final Long savingsAccountChargeId) {
this.context.authenticatedUser();
final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository
.findOneWithNotFoundDetection(savingsAccountChargeId, savingsAccountId);
final SavingsAccount account = savingsAccountCharge.savingsAccount();
this.savingAccountAssembler.assignSavingAccountHelpers(account);
final LocalDate inactivationOnDate = DateUtils.getBusinessLocalDate();
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(SAVINGS_ACCOUNT_CHARGE_RESOURCE_NAME);
// Only recurring fees are allowed to inactivate
if (!savingsAccountCharge.isRecurringFee()) {
baseDataValidator.reset().parameter(null).value(savingsAccountCharge.getId())
.failWithCodeNoParameterAddedToErrorCode("charge.inactivation.allowed.only.for.recurring.charges");
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
}
} else {
final LocalDate nextDueDate = savingsAccountCharge.getNextDueDateFrom(inactivationOnDate);
if (savingsAccountCharge.isChargeIsDue(nextDueDate)) {
baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("inactivation.of.charge.not.allowed.when.charge.is.due");
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
}
} else if (savingsAccountCharge.isChargeIsOverPaid(nextDueDate)) {
final List<SavingsAccountTransaction> chargePayments = new ArrayList<>();
SavingsAccountCharge updatedCharge = savingsAccountCharge;
do {
chargePayments.clear();
for (SavingsAccountTransaction transaction : account.getTransactions()) {
if (transaction.isPayCharge() && transaction.isNotReversed()
&& transaction.isPaymentForCurrentCharge(savingsAccountCharge)) {
chargePayments.add(transaction);
}
}
// Reverse the excess payments of charge transactions
SavingsAccountTransaction lastChargePayment = getLastChargePayment(chargePayments);
this.undoTransaction(savingsAccountCharge.savingsAccount().getId(), lastChargePayment.getId(), false);
updatedCharge = account.getUpdatedChargeDetails(savingsAccountCharge);
} while (updatedCharge.isChargeIsOverPaid(nextDueDate));
}
account.inactivateCharge(savingsAccountCharge, inactivationOnDate);
}
return new CommandProcessingResultBuilder() //
.withEntityId(savingsAccountCharge.getId()) //
.withOfficeId(savingsAccountCharge.savingsAccount().officeId()) //
.withClientId(savingsAccountCharge.savingsAccount().clientId()) //
.withGroupId(savingsAccountCharge.savingsAccount().groupId()) //
.withSavingsId(savingsAccountCharge.savingsAccount().getId()) //
.build();
}
private SavingsAccountTransaction getLastChargePayment(final List<SavingsAccountTransaction> chargePayments) {
if (!CollectionUtils.isEmpty(chargePayments)) {
return chargePayments.get(chargePayments.size() - 1);
}
return null;
}
@Transactional
@Override
public CommandProcessingResult assignFieldOfficer(Long savingsAccountId, JsonCommand command) {
this.context.authenticatedUser();
final Map<String, Object> actualChanges = new LinkedHashMap<>(5);
Staff fromSavingsOfficer = null;
Staff toSavingsOfficer = null;
this.fromApiJsonDeserializer.validateForAssignSavingsOfficer(command.json());
final SavingsAccount savingsForUpdate = this.savingAccountRepositoryWrapper.findOneWithNotFoundDetection(savingsAccountId);
final Long fromSavingsOfficerId = command.longValueOfParameterNamed("fromSavingsOfficerId");
final Long toSavingsOfficerId = command.longValueOfParameterNamed("toSavingsOfficerId");
final LocalDate dateOfSavingsOfficerAssignment = command.localDateValueOfParameterNamed("assignmentDate");
if (fromSavingsOfficerId != null) {
fromSavingsOfficer = this.staffRepository.findByOfficeHierarchyWithNotFoundDetection(fromSavingsOfficerId,
savingsForUpdate.office().getHierarchy());
}
if (toSavingsOfficerId != null) {
toSavingsOfficer = this.staffRepository.findByOfficeHierarchyWithNotFoundDetection(toSavingsOfficerId,
savingsForUpdate.office().getHierarchy());
actualChanges.put("toSavingsOfficerId", toSavingsOfficer.getId());
}
if (!savingsForUpdate.hasSavingsOfficer(fromSavingsOfficer)) {
throw new SavingsOfficerAssignmentException(savingsAccountId, fromSavingsOfficerId);
}
savingsForUpdate.reassignSavingsOfficer(toSavingsOfficer, dateOfSavingsOfficerAssignment);
this.savingAccountRepositoryWrapper.saveAndFlush(savingsForUpdate);
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withOfficeId(savingsForUpdate.officeId()) //
.withEntityId(savingsForUpdate.getId()) //
.withSavingsId(savingsAccountId) //
.with(actualChanges) //
.build();
}
@Transactional
@Override
public CommandProcessingResult unassignFieldOfficer(Long savingsAccountId, JsonCommand command) {
this.context.authenticatedUser();
final Map<String, Object> actualChanges = new LinkedHashMap<>(5);
this.fromApiJsonDeserializer.validateForUnAssignSavingsOfficer(command.json());
final SavingsAccount savingsForUpdate = this.savingAccountRepositoryWrapper.findOneWithNotFoundDetection(savingsAccountId);
if (savingsForUpdate.getSavingsOfficer() == null) {
throw new SavingsOfficerUnassignmentException(savingsAccountId);
}
final LocalDate dateOfSavingsOfficerUnassigned = command.localDateValueOfParameterNamed("unassignedDate");
savingsForUpdate.removeSavingsOfficer(dateOfSavingsOfficerUnassigned);
this.savingAccountRepositoryWrapper.saveAndFlush(savingsForUpdate);
actualChanges.put("toSavingsOfficerId", null);
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withOfficeId(savingsForUpdate.officeId()) //
.withEntityId(savingsForUpdate.getId()) //
.withSavingsId(savingsAccountId) //
.with(actualChanges) //
.build();
}
@Override
public CommandProcessingResult modifyWithHoldTax(Long savingsAccountId, JsonCommand command) {
final Map<String, Object> actualChanges = new HashMap<>(1);
final SavingsAccount savingsForUpdate = this.savingAccountRepositoryWrapper.findOneWithNotFoundDetection(savingsAccountId);
if (command.isChangeInBooleanParameterNamed(withHoldTaxParamName, savingsForUpdate.withHoldTax())) {
final boolean newValue = command.booleanPrimitiveValueOfParameterNamed(withHoldTaxParamName);
actualChanges.put(withHoldTaxParamName, newValue);
savingsForUpdate.setWithHoldTax(newValue);
if (savingsForUpdate.getTaxGroup() == null) {
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("account");
baseDataValidator.reset().parameter(withHoldTaxParamName).failWithCode("not.supported");
throw new PlatformApiDataValidationException(dataValidationErrors);
}
}
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(savingsAccountId) //
.withSavingsId(savingsAccountId) //
.with(actualChanges) //
.build();
}
@Override
public void setSubStatusInactive(Long savingsId) {
final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false);
final Set<Long> existingTransactionIds = new HashSet<>();
final Set<Long> existingReversedTransactionIds = new HashSet<>();
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
account.setSubStatusInactive(false);
this.savingAccountRepositoryWrapper.saveAndFlush(account);
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false);
}
@Override
public void setSubStatusDormant(Long savingsId) {
final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false);
account.setSubStatusDormant();
this.savingAccountRepositoryWrapper.saveAndFlush(account);
}
@Override
public void escheat(Long savingsId) {
final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false);
final Set<Long> existingTransactionIds = new HashSet<>();
final Set<Long> existingReversedTransactionIds = new HashSet<>();
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
account.escheat(appuserRepository.fetchSystemUser());
this.savingAccountRepositoryWrapper.saveAndFlush(account);
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false);
}
private AppUser getAppUserIfPresent() {
AppUser user = null;
if (this.context != null) {
user = this.context.getAuthenticatedUserIfPresent();
}
return user;
}
/**
* Disable all standing instructions linked to the savings account if the status is "closed"
*
* @param savingsAccount
* -- the savings account object
*
**/
@Transactional
private void disableStandingInstructionsLinkedToClosedSavings(final SavingsAccount savingsAccount) {
if (savingsAccount != null && savingsAccount.isClosed()) {
final Integer standingInstructionStatus = StandingInstructionStatus.ACTIVE.getValue();
final Collection<AccountTransferStandingInstruction> accountTransferStandingInstructions = this.standingInstructionRepository
.findBySavingsAccountAndStatus(savingsAccount, standingInstructionStatus);
if (!accountTransferStandingInstructions.isEmpty()) {
for (AccountTransferStandingInstruction accountTransferStandingInstruction : accountTransferStandingInstructions) {
accountTransferStandingInstruction.updateStatus(StandingInstructionStatus.DISABLED.getValue());
this.standingInstructionRepository.save(accountTransferStandingInstruction);
}
}
}
}
@Override
public CommandProcessingResult blockAccount(final Long savingsId, final JsonCommand command) {
this.context.authenticatedUser();
final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false);
checkClientOrGroupActive(account);
final Map<String, Object> changes = account.block();
final String reasonForBlock = command.stringValueOfParameterNamed(SavingsApiConstants.reasonForBlockParamName);
validateReasonForHold(reasonForBlock);
account.updateReason(reasonForBlock);
if (!changes.isEmpty()) {
this.savingAccountRepositoryWrapper.save(account);
}
return new CommandProcessingResultBuilder().withEntityId(savingsId).withOfficeId(account.officeId())
.withClientId(account.clientId()).withGroupId(account.groupId()).withSavingsId(savingsId).with(changes).build();
}
@Override
public CommandProcessingResult unblockAccount(final Long savingsId) {
this.context.authenticatedUser();
final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false);
checkClientOrGroupActive(account);
final Map<String, Object> changes = account.unblock();
account.updateReason(null);
if (!changes.isEmpty()) {
this.savingAccountRepositoryWrapper.save(account);
}
return new CommandProcessingResultBuilder().withEntityId(savingsId).withOfficeId(account.officeId())
.withClientId(account.clientId()).withGroupId(account.groupId()).withSavingsId(savingsId).with(changes).build();
}
@Transactional
@Override
public CommandProcessingResult holdAmount(final Long savingsId, final JsonCommand command) {
final AppUser submittedBy = this.context.authenticatedUser();
final boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus();
final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, backdatedTxnsAllowedTill);
final LocalDate transactionDate = command.localDateValueOfParameterNamed(transactionDateParamName);
final boolean lienAllowed = command.booleanPrimitiveValueOfParameterNamed(lienAllowedParamName);
checkClientOrGroupActive(account);
final BigDecimal amount = command.bigDecimalValueOfParameterNamed(transactionAmountParamName);
Money runningBalance = Money.of(account.getCurrency(), account.getAccountBalance());
if (account.getSavingsHoldAmount() != null) {
runningBalance = runningBalance.minus(account.getSavingsHoldAmount()).minus(amount);
} else {
runningBalance = runningBalance.minus(amount);
}
this.savingsAccountTransactionDataValidator.validateHoldAndAssembleForm(command.json(), account, submittedBy,
backdatedTxnsAllowedTill);
SavingsAccountTransaction transaction = this.savingsAccountDomainService.handleHold(account, amount, transactionDate, lienAllowed);
account.holdAmount(amount);
transaction.setRunningBalance(runningBalance);
final String reasonForBlock = command.stringValueOfParameterNamed(SavingsApiConstants.reasonForBlockParamName);
transaction.updateReason(reasonForBlock);
account.getAccountBalance();
this.savingsAccountTransactionDataValidator.validateTransactionWithPivotDate(transaction.getTransactionDate(), account);
this.savingsAccountTransactionRepository.saveAndFlush(transaction);
if (backdatedTxnsAllowedTill) {
// Check again whether transactions are modified
this.savingsAccountTransactionRepository.saveAll(account.getSavingsAccountTransactionsWithPivotConfig());
}
this.savingAccountRepositoryWrapper.saveAndFlush(account);
return new CommandProcessingResultBuilder().withEntityId(transaction.getId()).withOfficeId(account.officeId())
.withClientId(account.clientId()).withGroupId(account.groupId()).withSavingsId(savingsId).build();
}
@Transactional
@Override
public CommandProcessingResult releaseAmount(final Long savingsId, final Long savingsTransactionId) {
context.authenticatedUser();
SavingsAccountTransaction holdTransaction = this.savingsAccountTransactionRepository
.findOneByIdAndSavingsAccountId(savingsTransactionId, savingsId);
holdTransaction.updateReason(null);
final SavingsAccountTransaction transaction = this.savingsAccountTransactionDataValidator
.validateReleaseAmountAndAssembleForm(holdTransaction);
final boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus();
final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, backdatedTxnsAllowedTill);
checkClientOrGroupActive(account);
Money runningBalance = Money.of(account.getCurrency(), account.getAccountBalance());
Money savingsOnHold = Money.of(account.getCurrency(), account.getSavingsHoldAmount());
runningBalance = runningBalance.minus(savingsOnHold);
runningBalance = runningBalance.plus(transaction.getAmount());
transaction.setRunningBalance(runningBalance);
this.savingsAccountTransactionDataValidator.validateTransactionWithPivotDate(transaction.getTransactionDate(), account);
account.releaseOnHoldAmount(transaction.getAmount());
this.savingsAccountTransactionRepository.saveAndFlush(transaction);
holdTransaction.updateReleaseId(transaction.getId());
if (backdatedTxnsAllowedTill) {
this.savingsAccountTransactionRepository.saveAll(account.getSavingsAccountTransactionsWithPivotConfig());
} else {
account.addTransaction(transaction);
}
this.savingAccountRepositoryWrapper.save(account);
return new CommandProcessingResultBuilder().withEntityId(transaction.getId()).withOfficeId(account.officeId())
.withClientId(account.clientId()).withGroupId(account.groupId()).withSavingsId(account.getId()).build();
}
@Transactional
@Override
public CommandProcessingResult blockCredits(final Long savingsId, final JsonCommand command) {
this.context.authenticatedUser();
final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false);
checkClientOrGroupActive(account);
final String reasonForBlock = command.stringValueOfParameterNamed(SavingsApiConstants.reasonForBlockParamName);
validateReasonForHold(reasonForBlock);
account.updateReason(reasonForBlock);
final Map<String, Object> changes = account.blockCredits(account.getSubStatus());
if (!changes.isEmpty()) {
this.savingAccountRepositoryWrapper.save(account);
}
return new CommandProcessingResultBuilder().withEntityId(savingsId).withOfficeId(account.officeId())
.withClientId(account.clientId()).withGroupId(account.groupId()).withSavingsId(savingsId).with(changes).build();
}
@Transactional
@Override
public CommandProcessingResult unblockCredits(final Long savingsId) {
this.context.authenticatedUser();
final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false);
checkClientOrGroupActive(account);
account.updateReason(null);
final Map<String, Object> changes = account.unblockCredits();
if (!changes.isEmpty()) {
this.savingAccountRepositoryWrapper.save(account);
}
return new CommandProcessingResultBuilder().withEntityId(savingsId).withOfficeId(account.officeId())
.withClientId(account.clientId()).withGroupId(account.groupId()).withSavingsId(savingsId).with(changes).build();
}
@Transactional
@Override
public CommandProcessingResult blockDebits(final Long savingsId, final JsonCommand command) {
this.context.authenticatedUser();
final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false);
checkClientOrGroupActive(account);
final String reasonForBlock = command.stringValueOfParameterNamed(SavingsApiConstants.reasonForBlockParamName);
validateReasonForHold(reasonForBlock);
account.updateReason(reasonForBlock);
final Map<String, Object> changes = account.blockDebits(account.getSubStatus());
if (!changes.isEmpty()) {
this.savingAccountRepositoryWrapper.save(account);
}
return new CommandProcessingResultBuilder().withEntityId(savingsId).withOfficeId(account.officeId())
.withClientId(account.clientId()).withGroupId(account.groupId()).withSavingsId(savingsId).with(changes).build();
}
@Transactional
@Override
public CommandProcessingResult unblockDebits(final Long savingsId) {
this.context.authenticatedUser();
final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false);
checkClientOrGroupActive(account);
account.updateReason(null);
final Map<String, Object> changes = account.unblockDebits();
if (!changes.isEmpty()) {
this.savingAccountRepositoryWrapper.save(account);
}
return new CommandProcessingResultBuilder().withEntityId(savingsId).withOfficeId(account.officeId())
.withClientId(account.clientId()).withGroupId(account.groupId()).withSavingsId(savingsId).with(changes).build();
}
private void validateTransactionsForTransfer(final SavingsAccount savingsAccount, final LocalDate transferDate) {
for (SavingsAccountTransaction transaction : savingsAccount.getTransactions()) {
if ((DateUtils.isEqual(transferDate, transaction.getTransactionDate())
&& DateUtils.isEqual(transferDate, transaction.getSubmittedOnDate()))
|| DateUtils.isBefore(transferDate, transaction.getTransactionDate())) {
throw new GeneralPlatformDomainRuleException(TransferApiConstants.transferClientSavingsException,
TransferApiConstants.transferClientSavingsException, transaction.getCreatedDateTime(), transferDate);
}
}
}
private void validateReasonForHold(String reasonForBlock) {
if (StringUtils.isBlank(reasonForBlock)) {
throw new PlatformDataIntegrityException("Reason For Block is Mandatory", "error.msg.reason.for.block.mandatory");
}
}
}