blob: bcdb3e0da874d7ab83af41345724ffaa12f2d383 [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.loanaccount.service;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
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.Locale;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang.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.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.exception.PlatformServiceUnavailableException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.core.service.DateUtils;
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.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.organisation.holiday.domain.Holiday;
import org.apache.fineract.organisation.holiday.domain.HolidayRepositoryWrapper;
import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency;
import org.apache.fineract.organisation.monetary.domain.ApplicationCurrencyRepositoryWrapper;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.organisation.staff.domain.Staff;
import org.apache.fineract.organisation.workingdays.domain.WorkingDays;
import org.apache.fineract.organisation.workingdays.domain.WorkingDaysRepositoryWrapper;
import org.apache.fineract.portfolio.account.PortfolioAccountType;
import org.apache.fineract.portfolio.account.data.AccountTransferDTO;
import org.apache.fineract.portfolio.account.data.PortfolioAccountData;
import org.apache.fineract.portfolio.account.domain.AccountAssociationType;
import org.apache.fineract.portfolio.account.domain.AccountAssociations;
import org.apache.fineract.portfolio.account.domain.AccountAssociationsRepository;
import org.apache.fineract.portfolio.account.domain.AccountTransferDetailRepository;
import org.apache.fineract.portfolio.account.domain.AccountTransferDetails;
import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType;
import org.apache.fineract.portfolio.account.domain.AccountTransferRepository;
import org.apache.fineract.portfolio.account.domain.AccountTransferStandingInstruction;
import org.apache.fineract.portfolio.account.domain.AccountTransferTransaction;
import org.apache.fineract.portfolio.account.domain.AccountTransferType;
import org.apache.fineract.portfolio.account.domain.StandingInstructionPriority;
import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus;
import org.apache.fineract.portfolio.account.domain.StandingInstructionType;
import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlatformService;
import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService;
import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService;
import org.apache.fineract.portfolio.accountdetails.domain.AccountType;
import org.apache.fineract.portfolio.calendar.domain.Calendar;
import org.apache.fineract.portfolio.calendar.domain.CalendarEntityType;
import org.apache.fineract.portfolio.calendar.domain.CalendarInstance;
import org.apache.fineract.portfolio.calendar.domain.CalendarInstanceRepository;
import org.apache.fineract.portfolio.calendar.domain.CalendarRepository;
import org.apache.fineract.portfolio.calendar.domain.CalendarType;
import org.apache.fineract.portfolio.charge.domain.Charge;
import org.apache.fineract.portfolio.charge.domain.ChargePaymentMode;
import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper;
import org.apache.fineract.portfolio.charge.exception.ChargeCannotBeUpdatedException;
import org.apache.fineract.portfolio.charge.exception.LoanChargeCannotBeAddedException;
import org.apache.fineract.portfolio.charge.exception.LoanChargeCannotBeDeletedException;
import org.apache.fineract.portfolio.charge.exception.LoanChargeCannotBePayedException;
import org.apache.fineract.portfolio.charge.exception.LoanChargeCannotBeUpdatedException;
import org.apache.fineract.portfolio.charge.exception.LoanChargeCannotBeWaivedException;
import org.apache.fineract.portfolio.charge.exception.LoanChargeNotFoundException;
import org.apache.fineract.portfolio.charge.exception.LoanChargeCannotBeDeletedException.LOAN_CHARGE_CANNOT_BE_DELETED_REASON;
import org.apache.fineract.portfolio.charge.exception.LoanChargeCannotBePayedException.LOAN_CHARGE_CANNOT_BE_PAYED_REASON;
import org.apache.fineract.portfolio.charge.exception.LoanChargeCannotBeUpdatedException.LOAN_CHARGE_CANNOT_BE_UPDATED_REASON;
import org.apache.fineract.portfolio.charge.exception.LoanChargeCannotBeWaivedException.LOAN_CHARGE_CANNOT_BE_WAIVED_REASON;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.client.exception.ClientNotActiveException;
import org.apache.fineract.portfolio.collectionsheet.command.CollectionSheetBulkDisbursalCommand;
import org.apache.fineract.portfolio.collectionsheet.command.CollectionSheetBulkRepaymentCommand;
import org.apache.fineract.portfolio.collectionsheet.command.SingleDisbursalCommand;
import org.apache.fineract.portfolio.collectionsheet.command.SingleRepaymentCommand;
import org.apache.fineract.portfolio.common.BusinessEventNotificationConstants.BUSINESS_ENTITY;
import org.apache.fineract.portfolio.common.BusinessEventNotificationConstants.BUSINESS_EVENTS;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.common.service.BusinessEventNotifierService;
import org.apache.fineract.portfolio.group.domain.Group;
import org.apache.fineract.portfolio.group.exception.GroupNotActiveException;
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
import org.apache.fineract.portfolio.loanaccount.command.LoanUpdateCommand;
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
import org.apache.fineract.portfolio.loanaccount.data.LoanChargeData;
import org.apache.fineract.portfolio.loanaccount.data.LoanChargePaidByData;
import org.apache.fineract.portfolio.loanaccount.data.LoanInstallmentChargeData;
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail;
import org.apache.fineract.portfolio.loanaccount.domain.DefaultLoanLifecycleStateMachine;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargeRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails;
import org.apache.fineract.portfolio.loanaccount.domain.LoanEvent;
import org.apache.fineract.portfolio.loanaccount.domain.LoanInstallmentCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine;
import org.apache.fineract.portfolio.loanaccount.domain.LoanOverdueInstallmentCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallmentRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
import org.apache.fineract.portfolio.loanaccount.domain.LoanSummaryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTrancheDisbursementCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
import org.apache.fineract.portfolio.loanaccount.exception.ExceedingTrancheCountException;
import org.apache.fineract.portfolio.loanaccount.exception.InvalidPaidInAdvanceAmountException;
import org.apache.fineract.portfolio.loanaccount.exception.LoanDisbursalException;
import org.apache.fineract.portfolio.loanaccount.exception.LoanMultiDisbursementException;
import org.apache.fineract.portfolio.loanaccount.exception.LoanOfficerAssignmentException;
import org.apache.fineract.portfolio.loanaccount.exception.LoanOfficerUnassignmentException;
import org.apache.fineract.portfolio.loanaccount.exception.LoanTransactionNotFoundException;
import org.apache.fineract.portfolio.loanaccount.exception.MultiDisbursementDataRequiredException;
import org.apache.fineract.portfolio.loanaccount.guarantor.service.GuarantorDomainService;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OverdueLoanScheduleData;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.DefaultScheduledDateGenerator;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelPeriod;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.ScheduledDateGenerator;
import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleHistoryWritePlatformService;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationCommandFromApiJsonHelper;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanEventApiJsonValidator;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanUpdateCommandFromApiJsonDeserializer;
import org.apache.fineract.portfolio.loanproduct.data.LoanOverdueDTO;
import org.apache.fineract.portfolio.loanproduct.data.LoanProductData;
import org.apache.fineract.portfolio.loanproduct.exception.InvalidCurrencyException;
import org.apache.fineract.portfolio.loanproduct.exception.LinkedAccountRequiredException;
import org.apache.fineract.portfolio.loanproduct.service.LoanProductReadPlatformService;
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.domain.SavingsAccount;
import org.apache.fineract.portfolio.savings.exception.InsufficientAccountBalanceException;
import org.apache.fineract.useradministration.domain.AppUser;
import org.joda.time.LocalDate;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
@Service
public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatformService {
private final static Logger logger = LoggerFactory.getLogger(LoanWritePlatformServiceJpaRepositoryImpl.class);
private final PlatformSecurityContext context;
private final LoanEventApiJsonValidator loanEventApiJsonValidator;
private final LoanUpdateCommandFromApiJsonDeserializer loanUpdateCommandFromApiJsonDeserializer;
private final LoanRepository loanRepository;
private final LoanAccountDomainService loanAccountDomainService;
private final NoteRepository noteRepository;
private final LoanTransactionRepository loanTransactionRepository;
private final LoanAssembler loanAssembler;
private final ChargeRepositoryWrapper chargeRepository;
private final LoanChargeRepository loanChargeRepository;
private final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepository;
private final JournalEntryWritePlatformService journalEntryWritePlatformService;
private final CalendarInstanceRepository calendarInstanceRepository;
private final PaymentDetailWritePlatformService paymentDetailWritePlatformService;
private final HolidayRepositoryWrapper holidayRepository;
private final ConfigurationDomainService configurationDomainService;
private final WorkingDaysRepositoryWrapper workingDaysRepository;
private final LoanProductReadPlatformService loanProductReadPlatformService;
private final AccountTransfersWritePlatformService accountTransfersWritePlatformService;
private final AccountTransfersReadPlatformService accountTransfersReadPlatformService;
private final AccountAssociationsReadPlatformService accountAssociationsReadPlatformService;
private final LoanChargeReadPlatformService loanChargeReadPlatformService;
private final LoanReadPlatformService loanReadPlatformService;
private final FromJsonHelper fromApiJsonHelper;
private final AccountTransferRepository accountTransferRepository;
private final CalendarRepository calendarRepository;
private final LoanRepaymentScheduleInstallmentRepository repaymentScheduleInstallmentRepository;
private final LoanScheduleHistoryWritePlatformService loanScheduleHistoryWritePlatformService;
private final LoanApplicationCommandFromApiJsonHelper loanApplicationCommandFromApiJsonHelper;
private final AccountAssociationsRepository accountAssociationRepository;
private final AccountTransferDetailRepository accountTransferDetailRepository;
private final BusinessEventNotifierService businessEventNotifierService;
private final GuarantorDomainService guarantorDomainService;
private final LoanUtilService loanUtilService;
private final LoanSummaryWrapper loanSummaryWrapper;
private final LoanRepaymentScheduleTransactionProcessorFactory transactionProcessingStrategy;
@Autowired
public LoanWritePlatformServiceJpaRepositoryImpl(final PlatformSecurityContext context,
final LoanEventApiJsonValidator loanEventApiJsonValidator,
final LoanUpdateCommandFromApiJsonDeserializer loanUpdateCommandFromApiJsonDeserializer, final LoanAssembler loanAssembler,
final LoanRepository loanRepository, final LoanAccountDomainService loanAccountDomainService,
final LoanTransactionRepository loanTransactionRepository, final NoteRepository noteRepository,
final ChargeRepositoryWrapper chargeRepository, final LoanChargeRepository loanChargeRepository,
final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepository,
final JournalEntryWritePlatformService journalEntryWritePlatformService,
final CalendarInstanceRepository calendarInstanceRepository,
final PaymentDetailWritePlatformService paymentDetailWritePlatformService, final HolidayRepositoryWrapper holidayRepository,
final ConfigurationDomainService configurationDomainService, final WorkingDaysRepositoryWrapper workingDaysRepository,
final LoanProductReadPlatformService loanProductReadPlatformService,
final AccountTransfersWritePlatformService accountTransfersWritePlatformService,
final AccountTransfersReadPlatformService accountTransfersReadPlatformService,
final AccountAssociationsReadPlatformService accountAssociationsReadPlatformService,
final LoanChargeReadPlatformService loanChargeReadPlatformService, final LoanReadPlatformService loanReadPlatformService,
final FromJsonHelper fromApiJsonHelper, final AccountTransferRepository accountTransferRepository,
final CalendarRepository calendarRepository,
final LoanRepaymentScheduleInstallmentRepository repaymentScheduleInstallmentRepository,
final LoanScheduleHistoryWritePlatformService loanScheduleHistoryWritePlatformService,
final LoanApplicationCommandFromApiJsonHelper loanApplicationCommandFromApiJsonHelper,
final AccountAssociationsRepository accountAssociationRepository,
final AccountTransferDetailRepository accountTransferDetailRepository,
final BusinessEventNotifierService businessEventNotifierService, final GuarantorDomainService guarantorDomainService,
final LoanUtilService loanUtilService, final LoanSummaryWrapper loanSummaryWrapper,
final LoanRepaymentScheduleTransactionProcessorFactory transactionProcessingStrategy) {
this.context = context;
this.loanEventApiJsonValidator = loanEventApiJsonValidator;
this.loanAssembler = loanAssembler;
this.loanRepository = loanRepository;
this.loanAccountDomainService = loanAccountDomainService;
this.loanTransactionRepository = loanTransactionRepository;
this.noteRepository = noteRepository;
this.chargeRepository = chargeRepository;
this.loanChargeRepository = loanChargeRepository;
this.applicationCurrencyRepository = applicationCurrencyRepository;
this.journalEntryWritePlatformService = journalEntryWritePlatformService;
this.loanUpdateCommandFromApiJsonDeserializer = loanUpdateCommandFromApiJsonDeserializer;
this.calendarInstanceRepository = calendarInstanceRepository;
this.paymentDetailWritePlatformService = paymentDetailWritePlatformService;
this.holidayRepository = holidayRepository;
this.configurationDomainService = configurationDomainService;
this.workingDaysRepository = workingDaysRepository;
this.loanProductReadPlatformService = loanProductReadPlatformService;
this.accountTransfersWritePlatformService = accountTransfersWritePlatformService;
this.accountTransfersReadPlatformService = accountTransfersReadPlatformService;
this.accountAssociationsReadPlatformService = accountAssociationsReadPlatformService;
this.loanChargeReadPlatformService = loanChargeReadPlatformService;
this.loanReadPlatformService = loanReadPlatformService;
this.fromApiJsonHelper = fromApiJsonHelper;
this.accountTransferRepository = accountTransferRepository;
this.calendarRepository = calendarRepository;
this.repaymentScheduleInstallmentRepository = repaymentScheduleInstallmentRepository;
this.loanScheduleHistoryWritePlatformService = loanScheduleHistoryWritePlatformService;
this.loanApplicationCommandFromApiJsonHelper = loanApplicationCommandFromApiJsonHelper;
this.accountAssociationRepository = accountAssociationRepository;
this.accountTransferDetailRepository = accountTransferDetailRepository;
this.businessEventNotifierService = businessEventNotifierService;
this.guarantorDomainService = guarantorDomainService;
this.loanUtilService = loanUtilService;
this.loanSummaryWrapper = loanSummaryWrapper;
this.transactionProcessingStrategy = transactionProcessingStrategy;
}
private LoanLifecycleStateMachine defaultLoanLifecycleStateMachine() {
final List<LoanStatus> allowedLoanStatuses = Arrays.asList(LoanStatus.values());
return new DefaultLoanLifecycleStateMachine(allowedLoanStatuses);
}
@Transactional
@Override
public CommandProcessingResult disburseLoan(final Long loanId, final JsonCommand command, Boolean isAccountTransfer) {
final AppUser currentUser = getAppUserIfPresent();
this.loanEventApiJsonValidator.validateDisbursement(command.json(), isAccountTransfer);
final Loan loan = this.loanAssembler.assembleFrom(loanId);
checkClientOrGroupActive(loan);
final LocalDate nextPossibleRepaymentDate = loan.getNextPossibleRepaymentDateForRescheduling();
final Date rescheduledRepaymentDate = command.DateValueOfParameterNamed("adjustRepaymentDate");
// check for product mix validations
checkForProductMixRestrictions(loan);
// validate actual disbursement date against meeting date
final CalendarInstance calendarInstance = this.calendarInstanceRepository.findCalendarInstaneByEntityId(loan.getId(),
CalendarEntityType.LOANS.getValue());
if (loan.isSyncDisbursementWithMeeting()) {
final LocalDate actualDisbursementDate = command.localDateValueOfParameterNamed("actualDisbursementDate");
this.loanEventApiJsonValidator.validateDisbursementDateWithMeetingDate(actualDisbursementDate, calendarInstance);
}
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_DISBURSAL,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
final List<Long> existingTransactionIds = new ArrayList<>();
final List<Long> existingReversedTransactionIds = new ArrayList<>();
final Map<String, Object> changes = new LinkedHashMap<>();
final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes);
// Recalculate first repayment date based in actual disbursement date.
final LocalDate actualDisbursementDate = command.localDateValueOfParameterNamed("actualDisbursementDate");
updateLoanCounters(loan, actualDisbursementDate);
Money amountBeforeAdjust = loan.getPrincpal();
loan.validateAccountStatus(LoanEvent.LOAN_DISBURSED);
boolean canDisburse = loan.canDisburse(actualDisbursementDate);
ChangedTransactionDetail changedTransactionDetail = null;
if (canDisburse) {
Money disburseAmount = loan.adjustDisburseAmount(command, actualDisbursementDate);
boolean recalculateSchedule = amountBeforeAdjust.isNotEqualTo(loan.getPrincpal());
final String txnExternalId = command.stringValueOfParameterNamedAllowingNull("externalId");
if (isAccountTransfer) {
disburseLoanToSavings(loan, command, disburseAmount, paymentDetail);
existingTransactionIds.addAll(loan.findExistingTransactionIds());
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
} else {
existingTransactionIds.addAll(loan.findExistingTransactionIds());
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
LoanTransaction disbursementTransaction = LoanTransaction.disbursement(loan.getOffice(), disburseAmount, paymentDetail,
actualDisbursementDate, txnExternalId, DateUtils.getLocalDateTimeOfTenant(), currentUser);
disbursementTransaction.updateLoan(loan);
loan.getLoanTransactions().add(disbursementTransaction);
}
LocalDate recalculateFrom = null;
ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
regenerateScheduleOnDisbursement(command, loan, recalculateSchedule, scheduleGeneratorDTO, nextPossibleRepaymentDate, rescheduledRepaymentDate);
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
this.loanScheduleHistoryWritePlatformService.createAndSaveLoanScheduleArchive(loan.fetchRepaymentScheduleInstallments(),
loan, null);
}
if(configurationDomainService.isPaymnetypeApplicableforDisbursementCharge()){
changedTransactionDetail = loan.disburse(currentUser, command, changes, scheduleGeneratorDTO,paymentDetail);
}else{
changedTransactionDetail = loan.disburse(currentUser, command, changes, scheduleGeneratorDTO,null);
}
}
if (!changes.isEmpty()) {
saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
final String noteText = command.stringValueOfParameterNamed("note");
if (StringUtils.isNotBlank(noteText)) {
final Note note = Note.loanNote(loan, noteText);
this.noteRepository.save(note);
}
if (changedTransactionDetail != null) {
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
this.loanTransactionRepository.save(mapEntry.getValue());
this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
}
}
// auto create standing instruction
createStandingInstruction(loan);
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
}
final Set<LoanCharge> loanCharges = loan.charges();
final Map<Long, BigDecimal> disBuLoanCharges = new HashMap<>();
for (final LoanCharge loanCharge : loanCharges) {
if (loanCharge.isDueAtDisbursement() && loanCharge.getChargePaymentMode().isPaymentModeAccountTransfer()
&& loanCharge.isChargePending()) {
disBuLoanCharges.put(loanCharge.getId(), loanCharge.amountOutstanding());
}
}
final Locale locale = command.extractLocale();
final DateTimeFormatter fmt = DateTimeFormat.forPattern(command.dateFormat()).withLocale(locale);
for (final Map.Entry<Long, BigDecimal> entrySet : disBuLoanCharges.entrySet()) {
final PortfolioAccountData savingAccountData = this.accountAssociationsReadPlatformService.retriveLoanLinkedAssociation(loanId);
final SavingsAccount fromSavingsAccount = null;
final boolean isRegularTransaction = true;
final boolean isExceptionForBalanceCheck = false;
final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(actualDisbursementDate, entrySet.getValue(),
PortfolioAccountType.SAVINGS, PortfolioAccountType.LOAN, savingAccountData.accountId(), loanId, "Loan Charge Payment",
locale, fmt, null, null, LoanTransactionType.REPAYMENT_AT_DISBURSEMENT.getValue(), entrySet.getKey(), null,
AccountTransferType.CHARGE_PAYMENT.getValue(), null, null, null, null, null, fromSavingsAccount, isRegularTransaction,
isExceptionForBalanceCheck);
this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO);
}
updateRecurringCalendarDatesForInterestRecalculation(loan);
this.loanAccountDomainService.recalculateAccruals(loan);
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_DISBURSAL,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(loan.getId()) //
.withOfficeId(loan.getOfficeId()) //
.withClientId(loan.getClientId()) //
.withGroupId(loan.getGroupId()) //
.withLoanId(loanId) //
.with(changes) //
.build();
}
/**
* create standing instruction for disbursed loan
*
* @param loan
* the disbursed loan
* @return void
**/
private void createStandingInstruction(Loan loan) {
if (loan.shouldCreateStandingInstructionAtDisbursement()) {
AccountAssociations accountAssociations = this.accountAssociationRepository.findByLoanIdAndType(loan.getId(),
AccountAssociationType.LINKED_ACCOUNT_ASSOCIATION.getValue());
if (accountAssociations != null) {
SavingsAccount linkedSavingsAccount = accountAssociations.linkedSavingsAccount();
// name is auto-generated
final String name = "To loan " + loan.getAccountNumber() + " from savings " + linkedSavingsAccount.getAccountNumber();
final Office fromOffice = loan.getOffice();
final Client fromClient = loan.getClient();
final Office toOffice = loan.getOffice();
final Client toClient = loan.getClient();
final Integer priority = StandingInstructionPriority.MEDIUM.getValue();
final Integer transferType = AccountTransferType.LOAN_REPAYMENT.getValue();
final Integer instructionType = StandingInstructionType.DUES.getValue();
final Integer status = StandingInstructionStatus.ACTIVE.getValue();
final Integer recurrenceType = AccountTransferRecurrenceType.AS_PER_DUES.getValue();
final LocalDate validFrom = new LocalDate();
AccountTransferDetails accountTransferDetails = AccountTransferDetails.savingsToLoanTransfer(fromOffice, fromClient,
linkedSavingsAccount, toOffice, toClient, loan, transferType);
AccountTransferStandingInstruction accountTransferStandingInstruction = AccountTransferStandingInstruction.create(
accountTransferDetails, name, priority, instructionType, status, null, validFrom, null, recurrenceType, null, null,
null);
accountTransferDetails.updateAccountTransferStandingInstruction(accountTransferStandingInstruction);
this.accountTransferDetailRepository.save(accountTransferDetails);
}
}
}
private void updateRecurringCalendarDatesForInterestRecalculation(final Loan loan) {
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()
&& loan.loanInterestRecalculationDetails().getRestFrequencyType().isSameAsRepayment()) {
final CalendarInstance calendarInstanceForInterestRecalculation = this.calendarInstanceRepository
.findByEntityIdAndEntityTypeIdAndCalendarTypeId(loan.loanInterestRecalculationDetailId(),
CalendarEntityType.LOAN_RECALCULATION_REST_DETAIL.getValue(), CalendarType.COLLECTION.getValue());
Calendar calendarForInterestRecalculation = calendarInstanceForInterestRecalculation.getCalendar();
calendarForInterestRecalculation.updateStartAndEndDate(loan.getDisbursementDate(), loan.getMaturityDate());
this.calendarRepository.save(calendarForInterestRecalculation);
}
}
private void saveAndFlushLoanWithDataIntegrityViolationChecks(final Loan loan) {
try {
List<LoanRepaymentScheduleInstallment> installments = loan.fetchRepaymentScheduleInstallments();
for (LoanRepaymentScheduleInstallment installment : installments) {
if (installment.getId() == null) {
this.repaymentScheduleInstallmentRepository.save(installment);
}
}
this.loanRepository.saveAndFlush(loan);
} catch (final DataIntegrityViolationException e) {
final Throwable realCause = e.getCause();
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.transaction");
if (realCause.getMessage().toLowerCase().contains("external_id_unique")) {
baseDataValidator.reset().parameter("externalId").failWithCode("value.must.be.unique");
}
if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist",
"Validation errors exist.", dataValidationErrors); }
}
}
private void saveLoanWithDataIntegrityViolationChecks(final Loan loan) {
try {
List<LoanRepaymentScheduleInstallment> installments = loan.fetchRepaymentScheduleInstallments();
for (LoanRepaymentScheduleInstallment installment : installments) {
if (installment.getId() == null) {
this.repaymentScheduleInstallmentRepository.save(installment);
}
}
this.loanRepository.save(loan);
} catch (final DataIntegrityViolationException e) {
final Throwable realCause = e.getCause();
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.transaction");
if (realCause.getMessage().toLowerCase().contains("external_id_unique")) {
baseDataValidator.reset().parameter("externalId").failWithCode("value.must.be.unique");
}
if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist",
"Validation errors exist.", dataValidationErrors); }
}
}
/****
* TODO Vishwas: Pair with Ashok and re-factor collection sheet code-base
*
* May of the changes made to disburseLoan aren't being made here, should
* refactor to reuse disburseLoan ASAP
*****/
@Transactional
@Override
public Map<String, Object> bulkLoanDisbursal(final JsonCommand command, final CollectionSheetBulkDisbursalCommand bulkDisbursalCommand,
Boolean isAccountTransfer) {
final AppUser currentUser = getAppUserIfPresent();
final SingleDisbursalCommand[] disbursalCommand = bulkDisbursalCommand.getDisburseTransactions();
final Map<String, Object> changes = new LinkedHashMap<>();
if (disbursalCommand == null) { return changes; }
final LocalDate nextPossibleRepaymentDate = null;
final Date rescheduledRepaymentDate = null;
for (int i = 0; i < disbursalCommand.length; i++) {
final SingleDisbursalCommand singleLoanDisbursalCommand = disbursalCommand[i];
final Loan loan = this.loanAssembler.assembleFrom(singleLoanDisbursalCommand.getLoanId());
checkClientOrGroupActive(loan);
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_DISBURSAL,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
final List<Long> existingTransactionIds = new ArrayList<>();
final List<Long> existingReversedTransactionIds = new ArrayList<>();
final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes);
// Bulk disbursement should happen on meeting date (mostly from
// collection sheet).
// FIXME: AA - this should be first meeting date based on
// disbursement date and next available meeting dates
// assuming repayment schedule won't regenerate because expected
// disbursement and actual disbursement happens on same date
final LocalDate actualDisbursementDate = command.localDateValueOfParameterNamed("actualDisbursementDate");
loan.validateAccountStatus(LoanEvent.LOAN_DISBURSED);
updateLoanCounters(loan, actualDisbursementDate);
boolean canDisburse = loan.canDisburse(actualDisbursementDate);
ChangedTransactionDetail changedTransactionDetail = null;
if (canDisburse) {
Money amountBeforeAdjust = loan.getPrincpal();
Money disburseAmount = loan.adjustDisburseAmount(command, actualDisbursementDate);
boolean recalculateSchedule = amountBeforeAdjust.isNotEqualTo(loan.getPrincpal());
final String txnExternalId = command.stringValueOfParameterNamedAllowingNull("externalId");
if (isAccountTransfer) {
disburseLoanToSavings(loan, command, disburseAmount, paymentDetail);
existingTransactionIds.addAll(loan.findExistingTransactionIds());
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
} else {
existingTransactionIds.addAll(loan.findExistingTransactionIds());
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
LoanTransaction disbursementTransaction = LoanTransaction.disbursement(loan.getOffice(), disburseAmount, paymentDetail,
actualDisbursementDate, txnExternalId, DateUtils.getLocalDateTimeOfTenant(), currentUser);
disbursementTransaction.updateLoan(loan);
loan.getLoanTransactions().add(disbursementTransaction);
}
LocalDate recalculateFrom = null;
final ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
regenerateScheduleOnDisbursement(command, loan, recalculateSchedule, scheduleGeneratorDTO, nextPossibleRepaymentDate, rescheduledRepaymentDate);
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
this.loanScheduleHistoryWritePlatformService.createAndSaveLoanScheduleArchive(
loan.fetchRepaymentScheduleInstallments(), loan, null);
}
if(configurationDomainService.isPaymnetypeApplicableforDisbursementCharge()){
changedTransactionDetail = loan.disburse(currentUser, command, changes, scheduleGeneratorDTO,paymentDetail);
}else{
changedTransactionDetail = loan.disburse(currentUser, command, changes, scheduleGeneratorDTO,null);
}
}
if (!changes.isEmpty()) {
saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
final String noteText = command.stringValueOfParameterNamed("note");
if (StringUtils.isNotBlank(noteText)) {
final Note note = Note.loanNote(loan, noteText);
this.noteRepository.save(note);
}
if (changedTransactionDetail != null) {
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
this.loanTransactionRepository.save(mapEntry.getValue());
this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
}
}
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
}
final Set<LoanCharge> loanCharges = loan.charges();
final Map<Long, BigDecimal> disBuLoanCharges = new HashMap<>();
for (final LoanCharge loanCharge : loanCharges) {
if (loanCharge.isDueAtDisbursement() && loanCharge.getChargePaymentMode().isPaymentModeAccountTransfer()
&& loanCharge.isChargePending()) {
disBuLoanCharges.put(loanCharge.getId(), loanCharge.amountOutstanding());
}
}
final Locale locale = command.extractLocale();
final DateTimeFormatter fmt = DateTimeFormat.forPattern(command.dateFormat()).withLocale(locale);
for (final Map.Entry<Long, BigDecimal> entrySet : disBuLoanCharges.entrySet()) {
final PortfolioAccountData savingAccountData = this.accountAssociationsReadPlatformService
.retriveLoanLinkedAssociation(loan.getId());
final SavingsAccount fromSavingsAccount = null;
final boolean isRegularTransaction = true;
final boolean isExceptionForBalanceCheck = false;
final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(actualDisbursementDate, entrySet.getValue(),
PortfolioAccountType.SAVINGS, PortfolioAccountType.LOAN, savingAccountData.accountId(), loan.getId(),
"Loan Charge Payment", locale, fmt, null, null, LoanTransactionType.REPAYMENT_AT_DISBURSEMENT.getValue(),
entrySet.getKey(), null, AccountTransferType.CHARGE_PAYMENT.getValue(), null, null, null, null, null,
fromSavingsAccount, isRegularTransaction, isExceptionForBalanceCheck);
this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO);
}
updateRecurringCalendarDatesForInterestRecalculation(loan);
this.loanAccountDomainService.recalculateAccruals(loan);
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_DISBURSAL,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
}
return changes;
}
@Transactional
@Override
public CommandProcessingResult undoLoanDisbursal(final Long loanId, final JsonCommand command) {
final AppUser currentUser = getAppUserIfPresent();
final Loan loan = this.loanAssembler.assembleFrom(loanId);
checkClientOrGroupActive(loan);
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_UNDO_DISBURSAL,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
removeLoanCycle(loan);
final List<Long> existingTransactionIds = new ArrayList<>();
final List<Long> existingReversedTransactionIds = new ArrayList<>();
//
final MonetaryCurrency currency = loan.getCurrency();
final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency);
final LocalDate recalculateFrom = null;
ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
final Map<String, Object> changes = loan.undoDisbursal(scheduleGeneratorDTO, existingTransactionIds,
existingReversedTransactionIds, currentUser);
if (!changes.isEmpty()) {
saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
this.accountTransfersWritePlatformService.reverseAllTransactions(loanId, PortfolioAccountType.LOAN);
String noteText = null;
if (command.hasParameter("note")) {
noteText = command.stringValueOfParameterNamed("note");
if (StringUtils.isNotBlank(noteText)) {
final Note note = Note.loanNote(loan, noteText);
this.noteRepository.save(note);
}
}
boolean isAccountTransfer = false;
final Map<String, Object> accountingBridgeData = loan.deriveAccountingBridgeData(applicationCurrency.toData(),
existingTransactionIds, existingReversedTransactionIds, isAccountTransfer);
this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData);
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_UNDO_DISBURSAL,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
}
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(loan.getId()) //
.withOfficeId(loan.getOfficeId()) //
.withClientId(loan.getClientId()) //
.withGroupId(loan.getGroupId()) //
.withLoanId(loanId) //
.with(changes) //
.build();
}
@Transactional
@Override
public CommandProcessingResult makeLoanRepayment(final Long loanId, final JsonCommand command, final boolean isRecoveryRepayment) {
this.loanEventApiJsonValidator.validateNewRepaymentTransaction(command.json());
final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount");
final String txnExternalId = command.stringValueOfParameterNamedAllowingNull("externalId");
final Map<String, Object> changes = new LinkedHashMap<>();
changes.put("transactionDate", command.stringValueOfParameterNamed("transactionDate"));
changes.put("transactionAmount", command.stringValueOfParameterNamed("transactionAmount"));
changes.put("locale", command.locale());
changes.put("dateFormat", command.dateFormat());
changes.put("paymentTypeId", command.stringValueOfParameterNamed("paymentTypeId"));
final String noteText = command.stringValueOfParameterNamed("note");
if (StringUtils.isNotBlank(noteText)) {
changes.put("note", noteText);
}
final Loan loan = this.loanAssembler.assembleFrom(loanId);
final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes);
final Boolean isHolidayValidationDone = false;
final HolidayDetailDTO holidayDetailDto = null;
boolean isAccountTransfer = false;
final CommandProcessingResultBuilder commandProcessingResultBuilder = new CommandProcessingResultBuilder();
this.loanAccountDomainService.makeRepayment(loan, commandProcessingResultBuilder, transactionDate, transactionAmount,
paymentDetail, noteText, txnExternalId, isRecoveryRepayment, isAccountTransfer, holidayDetailDto, isHolidayValidationDone);
return commandProcessingResultBuilder.withCommandId(command.commandId()) //
.withLoanId(loanId) //
.with(changes) //
.build();
}
@Transactional
@Override
public Map<String, Object> makeLoanBulkRepayment(final CollectionSheetBulkRepaymentCommand bulkRepaymentCommand) {
final SingleRepaymentCommand[] repaymentCommand = bulkRepaymentCommand.getLoanTransactions();
final Map<String, Object> changes = new LinkedHashMap<>();
final boolean isRecoveryRepayment = false;
if (repaymentCommand == null) { return changes; }
List<Long> transactionIds = new ArrayList<>();
boolean isAccountTransfer = false;
HolidayDetailDTO holidayDetailDTO = null;
Boolean isHolidayValidationDone = false;
final boolean allowTransactionsOnHoliday = this.configurationDomainService.allowTransactionsOnHolidayEnabled();
for (final SingleRepaymentCommand singleLoanRepaymentCommand : repaymentCommand) {
if (singleLoanRepaymentCommand != null) {
Loan loans = this.loanRepository.findOne(singleLoanRepaymentCommand.getLoanId());
final List<Holiday> holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(loans.getOfficeId(),
singleLoanRepaymentCommand.getTransactionDate().toDate());
final WorkingDays workingDays = this.workingDaysRepository.findOne();
final boolean allowTransactionsOnNonWorkingDay = this.configurationDomainService.allowTransactionsOnNonWorkingDayEnabled();
boolean isHolidayEnabled = false;
isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled();
holidayDetailDTO = new HolidayDetailDTO(isHolidayEnabled, holidays, workingDays, allowTransactionsOnHoliday,
allowTransactionsOnNonWorkingDay);
loans.validateRepaymentDateIsOnHoliday(singleLoanRepaymentCommand.getTransactionDate(),
holidayDetailDTO.isAllowTransactionsOnHoliday(), holidayDetailDTO.getHolidays());
loans.validateRepaymentDateIsOnNonWorkingDay(singleLoanRepaymentCommand.getTransactionDate(),
holidayDetailDTO.getWorkingDays(), holidayDetailDTO.isAllowTransactionsOnNonWorkingDay());
isHolidayValidationDone = true;
break;
}
}
for (final SingleRepaymentCommand singleLoanRepaymentCommand : repaymentCommand) {
if (singleLoanRepaymentCommand != null) {
final Loan loan = this.loanAssembler.assembleFrom(singleLoanRepaymentCommand.getLoanId());
final PaymentDetail paymentDetail = singleLoanRepaymentCommand.getPaymentDetail();
if (paymentDetail != null && paymentDetail.getId() == null) {
this.paymentDetailWritePlatformService.persistPaymentDetail(paymentDetail);
}
final CommandProcessingResultBuilder commandProcessingResultBuilder = new CommandProcessingResultBuilder();
LoanTransaction loanTransaction = this.loanAccountDomainService.makeRepayment(loan, commandProcessingResultBuilder,
bulkRepaymentCommand.getTransactionDate(), singleLoanRepaymentCommand.getTransactionAmount(), paymentDetail,
bulkRepaymentCommand.getNote(), null, isRecoveryRepayment, isAccountTransfer, holidayDetailDTO,
isHolidayValidationDone);
transactionIds.add(loanTransaction.getId());
}
}
changes.put("loanTransactions", transactionIds);
return changes;
}
@Transactional
@Override
public CommandProcessingResult adjustLoanTransaction(final Long loanId, final Long transactionId, final JsonCommand command) {
AppUser currentUser = getAppUserIfPresent();
this.loanEventApiJsonValidator.validateTransaction(command.json());
final Loan loan = this.loanAssembler.assembleFrom(loanId);
checkClientOrGroupActive(loan);
final LoanTransaction transactionToAdjust = this.loanTransactionRepository.findOne(transactionId);
if (transactionToAdjust == null) { throw new LoanTransactionNotFoundException(transactionId); }
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_ADJUST_TRANSACTION,
constructEntityMap(BUSINESS_ENTITY.LOAN_ADJUSTED_TRANSACTION, transactionToAdjust));
if (this.accountTransfersReadPlatformService.isAccountTransfer(transactionId, PortfolioAccountType.LOAN)) { throw new PlatformServiceUnavailableException(
"error.msg.loan.transfer.transaction.update.not.allowed", "Loan transaction:" + transactionId
+ " update not allowed as it involves in account transfer", transactionId); }
if (loan.isClosedWrittenOff()) { throw new PlatformServiceUnavailableException("error.msg.loan.written.off.update.not.allowed",
"Loan transaction:" + transactionId + " update not allowed as loan status is written off", transactionId); }
final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount");
final String txnExternalId = command.stringValueOfParameterNamedAllowingNull("externalId");
final Map<String, Object> changes = new LinkedHashMap<>();
changes.put("transactionDate", command.stringValueOfParameterNamed("transactionDate"));
changes.put("transactionAmount", command.stringValueOfParameterNamed("transactionAmount"));
changes.put("locale", command.locale());
changes.put("dateFormat", command.dateFormat());
changes.put("paymentTypeId", command.stringValueOfParameterNamed("paymentTypeId"));
final List<Long> existingTransactionIds = new ArrayList<>();
final List<Long> existingReversedTransactionIds = new ArrayList<>();
final Money transactionAmountAsMoney = Money.of(loan.getCurrency(), transactionAmount);
final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createPaymentDetail(command, changes);
LoanTransaction newTransactionDetail = LoanTransaction.repayment(loan.getOffice(), transactionAmountAsMoney, paymentDetail,
transactionDate, txnExternalId, DateUtils.getLocalDateTimeOfTenant(), currentUser);
if (transactionToAdjust.isInterestWaiver()) {
Money unrecognizedIncome = transactionAmountAsMoney.zero();
Money interestComponent = transactionAmountAsMoney;
if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) {
Money receivableInterest = loan.getReceivableInterest(transactionDate);
if (transactionAmountAsMoney.isGreaterThan(receivableInterest)) {
interestComponent = receivableInterest;
unrecognizedIncome = transactionAmountAsMoney.minus(receivableInterest);
}
}
newTransactionDetail = LoanTransaction.waiver(loan.getOffice(), loan, transactionAmountAsMoney, transactionDate,
interestComponent, unrecognizedIncome, DateUtils.getLocalDateTimeOfTenant(), currentUser);
}
LocalDate recalculateFrom = null;
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
recalculateFrom = transactionToAdjust.getTransactionDate().isAfter(transactionDate) ? transactionDate : transactionToAdjust
.getTransactionDate();
}
ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
final ChangedTransactionDetail changedTransactionDetail = loan.adjustExistingTransaction(newTransactionDetail,
defaultLoanLifecycleStateMachine(), transactionToAdjust, existingTransactionIds, existingReversedTransactionIds,
scheduleGeneratorDTO, currentUser);
if (newTransactionDetail.isGreaterThanZero(loan.getPrincpal().getCurrency())) {
if (paymentDetail != null) {
this.paymentDetailWritePlatformService.persistPaymentDetail(paymentDetail);
}
this.loanTransactionRepository.save(newTransactionDetail);
}
/***
* TODO Vishwas Batch save is giving me a
* HibernateOptimisticLockingFailureException, looping and saving for
* the time being, not a major issue for now as this loop is entered
* only in edge cases (when a adjustment is made before the latest
* payment recorded against the loan)
***/
saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
if (changedTransactionDetail != null) {
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
this.loanTransactionRepository.save(mapEntry.getValue());
// update loan with references to the newly created transactions
loan.getLoanTransactions().add(mapEntry.getValue());
this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
}
}
final String noteText = command.stringValueOfParameterNamed("note");
if (StringUtils.isNotBlank(noteText)) {
changes.put("note", noteText);
Note note = null;
/**
* If a new transaction is not created, associate note with the
* transaction to be adjusted
**/
if (newTransactionDetail.isGreaterThanZero(loan.getPrincpal().getCurrency())) {
note = Note.loanTransactionNote(loan, newTransactionDetail, noteText);
} else {
note = Note.loanTransactionNote(loan, transactionToAdjust, noteText);
}
this.noteRepository.save(note);
}
Collection<Long> transactionIds = new ArrayList<>();
for (LoanTransaction transaction : loan.getLoanTransactions()) {
if (transaction.isRefund() && transaction.isNotReversed()) {
transactionIds.add(transaction.getId());
}
}
if (!transactionIds.isEmpty()) {
this.accountTransfersWritePlatformService
.reverseTransfersWithFromAccountTransactions(transactionIds, PortfolioAccountType.LOAN);
loan.updateLoanSummarAndStatus();
}
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
this.loanAccountDomainService.recalculateAccruals(loan);
Map<BUSINESS_ENTITY, Object> entityMap = constructEntityMap(BUSINESS_ENTITY.LOAN_ADJUSTED_TRANSACTION, transactionToAdjust);
if (newTransactionDetail.isRepayment() && newTransactionDetail.isGreaterThanZero(loan.getPrincpal().getCurrency())) {
entityMap.put(BUSINESS_ENTITY.LOAN_TRANSACTION, newTransactionDetail);
}
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_ADJUST_TRANSACTION, entityMap);
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(transactionId) //
.withOfficeId(loan.getOfficeId()) //
.withClientId(loan.getClientId()) //
.withGroupId(loan.getGroupId()) //
.withLoanId(loanId) //
.with(changes) //
.build();
}
@Transactional
@Override
public CommandProcessingResult waiveInterestOnLoan(final Long loanId, final JsonCommand command) {
AppUser currentUser = getAppUserIfPresent();
this.loanEventApiJsonValidator.validateTransaction(command.json());
final Map<String, Object> changes = new LinkedHashMap<>();
changes.put("transactionDate", command.stringValueOfParameterNamed("transactionDate"));
changes.put("transactionAmount", command.stringValueOfParameterNamed("transactionAmount"));
changes.put("locale", command.locale());
changes.put("dateFormat", command.dateFormat());
final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount");
final Loan loan = this.loanAssembler.assembleFrom(loanId);
checkClientOrGroupActive(loan);
final List<Long> existingTransactionIds = new ArrayList<>();
final List<Long> existingReversedTransactionIds = new ArrayList<>();
final Money transactionAmountAsMoney = Money.of(loan.getCurrency(), transactionAmount);
Money unrecognizedIncome = transactionAmountAsMoney.zero();
Money interestComponent = transactionAmountAsMoney;
if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) {
Money receivableInterest = loan.getReceivableInterest(transactionDate);
if (transactionAmountAsMoney.isGreaterThan(receivableInterest)) {
interestComponent = receivableInterest;
unrecognizedIncome = transactionAmountAsMoney.minus(receivableInterest);
}
}
final LoanTransaction waiveInterestTransaction = LoanTransaction.waiver(loan.getOffice(), loan, transactionAmountAsMoney,
transactionDate, interestComponent, unrecognizedIncome, DateUtils.getLocalDateTimeOfTenant(), currentUser);
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_WAIVE_INTEREST,
constructEntityMap(BUSINESS_ENTITY.LOAN_TRANSACTION, waiveInterestTransaction));
LocalDate recalculateFrom = null;
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
recalculateFrom = transactionDate;
}
ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
final ChangedTransactionDetail changedTransactionDetail = loan.waiveInterest(waiveInterestTransaction,
defaultLoanLifecycleStateMachine(), existingTransactionIds, existingReversedTransactionIds, scheduleGeneratorDTO,
currentUser);
this.loanTransactionRepository.save(waiveInterestTransaction);
/***
* TODO Vishwas Batch save is giving me a
* HibernateOptimisticLockingFailureException, looping and saving for
* the time being, not a major issue for now as this loop is entered
* only in edge cases (when a waiver is made before the latest payment
* recorded against the loan)
***/
saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
if (changedTransactionDetail != null) {
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
this.loanTransactionRepository.save(mapEntry.getValue());
// update loan with references to the newly created transactions
loan.getLoanTransactions().add(mapEntry.getValue());
this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
}
}
final String noteText = command.stringValueOfParameterNamed("note");
if (StringUtils.isNotBlank(noteText)) {
changes.put("note", noteText);
final Note note = Note.loanTransactionNote(loan, waiveInterestTransaction, noteText);
this.noteRepository.save(note);
}
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
this.loanAccountDomainService.recalculateAccruals(loan);
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_WAIVE_INTEREST,
constructEntityMap(BUSINESS_ENTITY.LOAN_TRANSACTION, waiveInterestTransaction));
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(waiveInterestTransaction.getId()) //
.withOfficeId(loan.getOfficeId()) //
.withClientId(loan.getClientId()) //
.withGroupId(loan.getGroupId()) //
.withLoanId(loanId) //
.with(changes) //
.build();
}
@Transactional
@Override
public CommandProcessingResult writeOff(final Long loanId, final JsonCommand command) {
final AppUser currentUser = getAppUserIfPresent();
this.loanEventApiJsonValidator.validateTransactionWithNoAmount(command.json());
final Map<String, Object> changes = new LinkedHashMap<>();
changes.put("transactionDate", command.stringValueOfParameterNamed("transactionDate"));
changes.put("locale", command.locale());
changes.put("dateFormat", command.dateFormat());
final Loan loan = this.loanAssembler.assembleFrom(loanId);
checkClientOrGroupActive(loan);
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_WRITTEN_OFF,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
removeLoanCycle(loan);
final List<Long> existingTransactionIds = new ArrayList<>();
final List<Long> existingReversedTransactionIds = new ArrayList<>();
updateLoanCounters(loan, loan.getDisbursementDate());
LocalDate recalculateFrom = null;
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
recalculateFrom = command.localDateValueOfParameterNamed("transactionDate");
}
ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
final ChangedTransactionDetail changedTransactionDetail = loan.closeAsWrittenOff(command, defaultLoanLifecycleStateMachine(),
changes, existingTransactionIds, existingReversedTransactionIds, currentUser, scheduleGeneratorDTO);
LoanTransaction writeoff = changedTransactionDetail.getNewTransactionMappings().remove(0L);
this.loanTransactionRepository.save(writeoff);
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
this.loanTransactionRepository.save(mapEntry.getValue());
this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
}
saveLoanWithDataIntegrityViolationChecks(loan);
final String noteText = command.stringValueOfParameterNamed("note");
if (StringUtils.isNotBlank(noteText)) {
changes.put("note", noteText);
final Note note = Note.loanTransactionNote(loan, writeoff, noteText);
this.noteRepository.save(note);
}
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
this.loanAccountDomainService.recalculateAccruals(loan);
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_WRITTEN_OFF,
constructEntityMap(BUSINESS_ENTITY.LOAN_TRANSACTION, writeoff));
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(writeoff.getId()) //
.withOfficeId(loan.getOfficeId()) //
.withClientId(loan.getClientId()) //
.withGroupId(loan.getGroupId()) //
.withLoanId(loanId) //
.with(changes) //
.build();
}
@Transactional
@Override
public CommandProcessingResult closeLoan(final Long loanId, final JsonCommand command) {
AppUser currentUser = getAppUserIfPresent();
this.loanEventApiJsonValidator.validateTransactionWithNoAmount(command.json());
final Loan loan = this.loanAssembler.assembleFrom(loanId);
checkClientOrGroupActive(loan);
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_CLOSE,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
final Map<String, Object> changes = new LinkedHashMap<>();
changes.put("transactionDate", command.stringValueOfParameterNamed("transactionDate"));
changes.put("locale", command.locale());
changes.put("dateFormat", command.dateFormat());
final List<Long> existingTransactionIds = new ArrayList<>();
final List<Long> existingReversedTransactionIds = new ArrayList<>();
updateLoanCounters(loan, loan.getDisbursementDate());
LocalDate recalculateFrom = null;
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
recalculateFrom = command.localDateValueOfParameterNamed("transactionDate");
}
ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
ChangedTransactionDetail changedTransactionDetail = loan.close(command, defaultLoanLifecycleStateMachine(), changes,
existingTransactionIds, existingReversedTransactionIds, scheduleGeneratorDTO, currentUser);
final LoanTransaction possibleClosingTransaction = changedTransactionDetail.getNewTransactionMappings().remove(0L);
if (possibleClosingTransaction != null) {
this.loanTransactionRepository.save(possibleClosingTransaction);
}
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
this.loanTransactionRepository.save(mapEntry.getValue());
this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
}
saveLoanWithDataIntegrityViolationChecks(loan);
final String noteText = command.stringValueOfParameterNamed("note");
if (StringUtils.isNotBlank(noteText)) {
changes.put("note", noteText);
final Note note = Note.loanNote(loan, noteText);
this.noteRepository.save(note);
}
if (possibleClosingTransaction != null) {
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
}
this.loanAccountDomainService.recalculateAccruals(loan);
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_CLOSE,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
CommandProcessingResult result = null;
if (possibleClosingTransaction != null) {
result = new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(possibleClosingTransaction.getId()) //
.withOfficeId(loan.getOfficeId()) //
.withClientId(loan.getClientId()) //
.withGroupId(loan.getGroupId()) //
.withLoanId(loanId) //
.with(changes) //
.build();
} else {
result = new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(loanId) //
.withOfficeId(loan.getOfficeId()) //
.withClientId(loan.getClientId()) //
.withGroupId(loan.getGroupId()) //
.withLoanId(loanId) //
.with(changes) //
.build();
}
return result;
}
@Transactional
@Override
public CommandProcessingResult closeAsRescheduled(final Long loanId, final JsonCommand command) {
this.loanEventApiJsonValidator.validateTransactionWithNoAmount(command.json());
final Loan loan = this.loanAssembler.assembleFrom(loanId);
checkClientOrGroupActive(loan);
removeLoanCycle(loan);
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_CLOSE_AS_RESCHEDULE,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
final Map<String, Object> changes = new LinkedHashMap<>();
changes.put("transactionDate", command.stringValueOfParameterNamed("transactionDate"));
changes.put("locale", command.locale());
changes.put("dateFormat", command.dateFormat());
loan.closeAsMarkedForReschedule(command, defaultLoanLifecycleStateMachine(), changes);
saveLoanWithDataIntegrityViolationChecks(loan);
final String noteText = command.stringValueOfParameterNamed("note");
if (StringUtils.isNotBlank(noteText)) {
changes.put("note", noteText);
final Note note = Note.loanNote(loan, noteText);
this.noteRepository.save(note);
}
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_CLOSE_AS_RESCHEDULE,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(loanId) //
.withOfficeId(loan.getOfficeId()) //
.withClientId(loan.getClientId()) //
.withGroupId(loan.getGroupId()) //
.withLoanId(loanId) //
.with(changes) //
.build();
}
private void validateAddingNewChargeAllowed(Set<LoanDisbursementDetails> loanDisburseDetails) {
boolean pendingDisbursementAvailable = false;
for (LoanDisbursementDetails disbursementDetail : loanDisburseDetails) {
if (disbursementDetail.actualDisbursementDate() == null) {
pendingDisbursementAvailable = true;
break;
}
}
if (!pendingDisbursementAvailable) { throw new ChargeCannotBeUpdatedException(
"error.msg.charge.cannot.be.updated.no.pending.disbursements.in.loan",
"This charge cannot be added, No disbursement is pending"); }
}
@Transactional
@Override
public CommandProcessingResult addLoanCharge(final Long loanId, final JsonCommand command) {
this.loanEventApiJsonValidator.validateAddLoanCharge(command.json());
final Loan loan = this.loanAssembler.assembleFrom(loanId);
checkClientOrGroupActive(loan);
Set<LoanDisbursementDetails> loanDisburseDetails = loan.getDisbursementDetails();
final Long chargeDefinitionId = command.longValueOfParameterNamed("chargeId");
final Charge chargeDefinition = this.chargeRepository.findOneWithNotFoundDetection(chargeDefinitionId);
if (loan.isDisbursed() && chargeDefinition.isDisbursementCharge()) {
validateAddingNewChargeAllowed(loanDisburseDetails); // validates
// whether any
// pending
// disbursements
// are
// available to
// apply this
// charge
}
final List<Long> existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds());
final List<Long> existingReversedTransactionIds = new ArrayList<>(loan.findExistingReversedTransactionIds());
boolean isAppliedOnBackDate = false;
LoanCharge loanCharge = null;
LocalDate recalculateFrom = loan.fetchInterestRecalculateFromDate();
if (chargeDefinition.isPercentageOfDisbursementAmount()) {
LoanTrancheDisbursementCharge loanTrancheDisbursementCharge = null;
for (LoanDisbursementDetails disbursementDetail : loanDisburseDetails) {
if (disbursementDetail.actualDisbursementDate() == null) {
loanCharge = LoanCharge.createNewWithoutLoan(chargeDefinition, disbursementDetail.principal(), null, null, null,
disbursementDetail.expectedDisbursementDateAsLocalDate(), null, null);
loanTrancheDisbursementCharge = new LoanTrancheDisbursementCharge(loanCharge, disbursementDetail);
loanCharge.updateLoanTrancheDisbursementCharge(loanTrancheDisbursementCharge);
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_ADD_CHARGE,
constructEntityMap(BUSINESS_ENTITY.LOAN_CHARGE, loanCharge));
validateAddLoanCharge(loan, chargeDefinition, loanCharge);
addCharge(loan, chargeDefinition, loanCharge);
isAppliedOnBackDate = true;
if (recalculateFrom.isAfter(disbursementDetail.expectedDisbursementDateAsLocalDate())) {
recalculateFrom = disbursementDetail.expectedDisbursementDateAsLocalDate();
}
}
}
loan.addTrancheLoanCharge(chargeDefinition);
} else {
loanCharge = LoanCharge.createNewFromJson(loan, chargeDefinition, command);
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_ADD_CHARGE,
constructEntityMap(BUSINESS_ENTITY.LOAN_CHARGE, loanCharge));
validateAddLoanCharge(loan, chargeDefinition, loanCharge);
isAppliedOnBackDate = addCharge(loan, chargeDefinition, loanCharge);
if (loanCharge.getDueLocalDate() == null || recalculateFrom.isAfter(loanCharge.getDueLocalDate())) {
isAppliedOnBackDate = true;
recalculateFrom = loanCharge.getDueLocalDate();
}
}
boolean reprocessRequired = true;
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
if (isAppliedOnBackDate && loan.isFeeCompoundingEnabledForInterestRecalculation()) {
runScheduleRecalculation(loan, recalculateFrom);
reprocessRequired = false;
}
updateOriginalSchedule(loan);
}
if (reprocessRequired) {
ChangedTransactionDetail changedTransactionDetail = loan.reprocessTransactions();
if (changedTransactionDetail != null) {
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
this.loanTransactionRepository.save(mapEntry.getValue());
// update loan with references to the newly created
// transactions
loan.getLoanTransactions().add(mapEntry.getValue());
this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
}
}
saveLoanWithDataIntegrityViolationChecks(loan);
}
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled() && isAppliedOnBackDate
&& loan.isFeeCompoundingEnabledForInterestRecalculation()) {
this.loanAccountDomainService.recalculateAccruals(loan);
}
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_ADD_CHARGE,
constructEntityMap(BUSINESS_ENTITY.LOAN_CHARGE, loanCharge));
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(loanCharge.getId()) //
.withOfficeId(loan.getOfficeId()) //
.withClientId(loan.getClientId()) //
.withGroupId(loan.getGroupId()) //
.withLoanId(loanId) //
.build();
}
private void validateAddLoanCharge(final Loan loan, final Charge chargeDefinition, final LoanCharge loanCharge) {
if (chargeDefinition.isOverdueInstallment()) {
final String defaultUserMessage = "Installment charge cannot be added to the loan.";
throw new LoanChargeCannotBeAddedException("loanCharge", "overdue.charge", defaultUserMessage, null, chargeDefinition.getName());
} else if (loanCharge.getDueLocalDate() != null
&& loanCharge.getDueLocalDate().isBefore(loan.getLastUserTransactionForChargeCalc())) {
final String defaultUserMessage = "charge with date before last transaction date can not be added to loan.";
throw new LoanChargeCannotBeAddedException("loanCharge", "date.is.before.last.transaction.date", defaultUserMessage, null,
chargeDefinition.getName());
} else if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
if (loanCharge.isInstalmentFee() && loan.status().isActive()) {
final String defaultUserMessage = "installment charge addition not allowed after disbursement";
throw new LoanChargeCannotBeAddedException("loanCharge", "installment.charge", defaultUserMessage, null,
chargeDefinition.getName());
}
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final Set<LoanCharge> loanCharges = new HashSet<>(1);
loanCharges.add(loanCharge);
this.loanApplicationCommandFromApiJsonHelper.validateLoanCharges(loanCharges, dataValidationErrors);
if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); }
}
}
public void runScheduleRecalculation(final Loan loan, final LocalDate recalculateFrom) {
AppUser currentUser = getAppUserIfPresent();
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
ScheduleGeneratorDTO generatorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
ChangedTransactionDetail changedTransactionDetail = loan.handleRegenerateRepaymentScheduleWithInterestRecalculation(
generatorDTO, currentUser);
saveLoanWithDataIntegrityViolationChecks(loan);
if (changedTransactionDetail != null) {
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
this.loanTransactionRepository.save(mapEntry.getValue());
// update loan with references to the newly created
// transactions
loan.getLoanTransactions().add(mapEntry.getValue());
this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
}
}
}
}
public void updateOriginalSchedule(Loan loan) {
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
final LocalDate recalculateFrom = null;
ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
createLoanScheduleArchive(loan, scheduleGeneratorDTO);
}
}
private boolean addCharge(final Loan loan, final Charge chargeDefinition, final LoanCharge loanCharge) {
AppUser currentUser = getAppUserIfPresent();
if (!loan.hasCurrencyCodeOf(chargeDefinition.getCurrencyCode())) {
final String errorMessage = "Charge and Loan must have the same currency.";
throw new InvalidCurrencyException("loanCharge", "attach.to.loan", errorMessage);
}
if (loanCharge.getChargePaymentMode().isPaymentModeAccountTransfer()) {
final PortfolioAccountData portfolioAccountData = this.accountAssociationsReadPlatformService.retriveLoanLinkedAssociation(loan
.getId());
if (portfolioAccountData == null) {
final String errorMessage = loanCharge.name() + "Charge requires linked savings account for payment";
throw new LinkedAccountRequiredException("loanCharge.add", errorMessage, loanCharge.name());
}
}
loan.addLoanCharge(loanCharge);
this.loanChargeRepository.save(loanCharge);
/**
* we want to apply charge transactions only for those loans charges
* that are applied when a loan is active and the loan product uses
* Upfront Accruals
**/
if (loan.status().isActive() && loan.isNoneOrCashOrUpfrontAccrualAccountingEnabledOnLoanProduct()) {
final LoanTransaction applyLoanChargeTransaction = loan.handleChargeAppliedTransaction(loanCharge, null, currentUser);
this.loanTransactionRepository.save(applyLoanChargeTransaction);
}
boolean isAppliedOnBackDate = false;
if (loanCharge.getDueLocalDate() == null || LocalDate.now().isAfter(loanCharge.getDueLocalDate())) {
isAppliedOnBackDate = true;
}
return isAppliedOnBackDate;
}
@Transactional
@Override
public CommandProcessingResult updateLoanCharge(final Long loanId, final Long loanChargeId, final JsonCommand command) {
this.loanEventApiJsonValidator.validateUpdateOfLoanCharge(command.json());
final Loan loan = this.loanAssembler.assembleFrom(loanId);
checkClientOrGroupActive(loan);
final LoanCharge loanCharge = retrieveLoanChargeBy(loanId, loanChargeId);
// Charges may be edited only when the loan associated with them are
// yet to be approved (are in submitted and pending status)
if (!loan.status().isSubmittedAndPendingApproval()) { throw new LoanChargeCannotBeUpdatedException(
LOAN_CHARGE_CANNOT_BE_UPDATED_REASON.LOAN_NOT_IN_SUBMITTED_AND_PENDING_APPROVAL_STAGE, loanCharge.getId()); }
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_UPDATE_CHARGE,
constructEntityMap(BUSINESS_ENTITY.LOAN_CHARGE, loanCharge));
final Map<String, Object> changes = loan.updateLoanCharge(loanCharge, command);
saveLoanWithDataIntegrityViolationChecks(loan);
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_UPDATE_CHARGE,
constructEntityMap(BUSINESS_ENTITY.LOAN_CHARGE, loanCharge));
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(loanChargeId) //
.withOfficeId(loan.getOfficeId()) //
.withClientId(loan.getClientId()) //
.withGroupId(loan.getGroupId()) //
.withLoanId(loanId) //
.with(changes) //
.build();
}
@Transactional
@Override
public CommandProcessingResult waiveLoanCharge(final Long loanId, final Long loanChargeId, final JsonCommand command) {
AppUser currentUser = getAppUserIfPresent();
final Loan loan = this.loanAssembler.assembleFrom(loanId);
checkClientOrGroupActive(loan);
this.loanEventApiJsonValidator.validateInstallmentChargeTransaction(command.json());
final LoanCharge loanCharge = retrieveLoanChargeBy(loanId, loanChargeId);
// Charges may be waived only when the loan associated with them are
// active
if (!loan.status().isActive()) { throw new LoanChargeCannotBeWaivedException(LOAN_CHARGE_CANNOT_BE_WAIVED_REASON.LOAN_INACTIVE,
loanCharge.getId()); }
// validate loan charge is not already paid or waived
if (loanCharge.isWaived()) {
throw new LoanChargeCannotBeWaivedException(LOAN_CHARGE_CANNOT_BE_WAIVED_REASON.ALREADY_WAIVED, loanCharge.getId());
} else if (loanCharge.isPaid()) { throw new LoanChargeCannotBeWaivedException(LOAN_CHARGE_CANNOT_BE_WAIVED_REASON.ALREADY_PAID,
loanCharge.getId()); }
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_WAIVE_CHARGE,
constructEntityMap(BUSINESS_ENTITY.LOAN_CHARGE, loanCharge));
Integer loanInstallmentNumber = null;
if (loanCharge.isInstalmentFee()) {
LoanInstallmentCharge chargePerInstallment = null;
if (!StringUtils.isBlank(command.json())) {
final LocalDate dueDate = command.localDateValueOfParameterNamed("dueDate");
final Integer installmentNumber = command.integerValueOfParameterNamed("installmentNumber");
if (dueDate != null) {
chargePerInstallment = loanCharge.getInstallmentLoanCharge(dueDate);
} else if (installmentNumber != null) {
chargePerInstallment = loanCharge.getInstallmentLoanCharge(installmentNumber);
}
}
if (chargePerInstallment == null) {
chargePerInstallment = loanCharge.getUnpaidInstallmentLoanCharge();
}
if (chargePerInstallment.isWaived()) {
throw new LoanChargeCannotBePayedException(LOAN_CHARGE_CANNOT_BE_PAYED_REASON.ALREADY_WAIVED, loanCharge.getId());
} else if (chargePerInstallment.isPaid()) { throw new LoanChargeCannotBePayedException(
LOAN_CHARGE_CANNOT_BE_PAYED_REASON.ALREADY_PAID, loanCharge.getId()); }
loanInstallmentNumber = chargePerInstallment.getRepaymentInstallment().getInstallmentNumber();
}
final Map<String, Object> changes = new LinkedHashMap<>(3);
final List<Long> existingTransactionIds = new ArrayList<>();
final List<Long> existingReversedTransactionIds = new ArrayList<>();
LocalDate recalculateFrom = null;
ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
Money accruedCharge = Money.zero(loan.getCurrency());
if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) {
Collection<LoanChargePaidByData> chargePaidByDatas = this.loanChargeReadPlatformService.retriveLoanChargesPaidBy(
loanCharge.getId(), LoanTransactionType.ACCRUAL, loanInstallmentNumber);
for (LoanChargePaidByData chargePaidByData : chargePaidByDatas) {
accruedCharge = accruedCharge.plus(chargePaidByData.getAmount());
}
}
final LoanTransaction waiveTransaction = loan.waiveLoanCharge(loanCharge, defaultLoanLifecycleStateMachine(), changes,
existingTransactionIds, existingReversedTransactionIds, loanInstallmentNumber, scheduleGeneratorDTO, accruedCharge,
currentUser);
this.loanTransactionRepository.save(waiveTransaction);
saveLoanWithDataIntegrityViolationChecks(loan);
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_WAIVE_CHARGE,
constructEntityMap(BUSINESS_ENTITY.LOAN_CHARGE, loanCharge));
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(loanChargeId) //
.withOfficeId(loan.getOfficeId()) //
.withClientId(loan.getClientId()) //
.withGroupId(loan.getGroupId()) //
.withLoanId(loanId) //
.with(changes) //
.build();
}
@Transactional
@Override
public CommandProcessingResult deleteLoanCharge(final Long loanId, final Long loanChargeId, final JsonCommand command) {
final Loan loan = this.loanAssembler.assembleFrom(loanId);
checkClientOrGroupActive(loan);
final LoanCharge loanCharge = retrieveLoanChargeBy(loanId, loanChargeId);
// Charges may be deleted only when the loan associated with them are
// yet to be approved (are in submitted and pending status)
if (!loan.status().isSubmittedAndPendingApproval()) { throw new LoanChargeCannotBeDeletedException(
LOAN_CHARGE_CANNOT_BE_DELETED_REASON.LOAN_NOT_IN_SUBMITTED_AND_PENDING_APPROVAL_STAGE, loanCharge.getId()); }
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_DELETE_CHARGE,
constructEntityMap(BUSINESS_ENTITY.LOAN_CHARGE, loanCharge));
loan.removeLoanCharge(loanCharge);
saveLoanWithDataIntegrityViolationChecks(loan);
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_DELETE_CHARGE,
constructEntityMap(BUSINESS_ENTITY.LOAN_CHARGE, loanCharge));
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(loanChargeId) //
.withOfficeId(loan.getOfficeId()) //
.withClientId(loan.getClientId()) //
.withGroupId(loan.getGroupId()) //
.withLoanId(loanId) //
.build();
}
@Override
@Transactional
public CommandProcessingResult payLoanCharge(final Long loanId, Long loanChargeId, final JsonCommand command,
final boolean isChargeIdIncludedInJson) {
this.loanEventApiJsonValidator.validateChargePaymentTransaction(command.json(), isChargeIdIncludedInJson);
if (isChargeIdIncludedInJson) {
loanChargeId = command.longValueOfParameterNamed("chargeId");
}
final Loan loan = this.loanAssembler.assembleFrom(loanId);
checkClientOrGroupActive(loan);
final LoanCharge loanCharge = retrieveLoanChargeBy(loanId, loanChargeId);
// Charges may be waived only when the loan associated with them are
// active
if (!loan.status().isActive()) { throw new LoanChargeCannotBePayedException(LOAN_CHARGE_CANNOT_BE_PAYED_REASON.LOAN_INACTIVE,
loanCharge.getId()); }
// validate loan charge is not already paid or waived
if (loanCharge.isWaived()) {
throw new LoanChargeCannotBePayedException(LOAN_CHARGE_CANNOT_BE_PAYED_REASON.ALREADY_WAIVED, loanCharge.getId());
} else if (loanCharge.isPaid()) { throw new LoanChargeCannotBePayedException(LOAN_CHARGE_CANNOT_BE_PAYED_REASON.ALREADY_PAID,
loanCharge.getId()); }
if (!loanCharge.getChargePaymentMode().isPaymentModeAccountTransfer()) { throw new LoanChargeCannotBePayedException(
LOAN_CHARGE_CANNOT_BE_PAYED_REASON.CHARGE_NOT_ACCOUNT_TRANSFER, loanCharge.getId()); }
final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
final Locale locale = command.extractLocale();
final DateTimeFormatter fmt = DateTimeFormat.forPattern(command.dateFormat()).withLocale(locale);
Integer loanInstallmentNumber = null;
BigDecimal amount = loanCharge.amountOutstanding();
if (loanCharge.isInstalmentFee()) {
LoanInstallmentCharge chargePerInstallment = null;
final LocalDate dueDate = command.localDateValueOfParameterNamed("dueDate");
final Integer installmentNumber = command.integerValueOfParameterNamed("installmentNumber");
if (dueDate != null) {
chargePerInstallment = loanCharge.getInstallmentLoanCharge(dueDate);
} else if (installmentNumber != null) {
chargePerInstallment = loanCharge.getInstallmentLoanCharge(installmentNumber);
}
if (chargePerInstallment == null) {
chargePerInstallment = loanCharge.getUnpaidInstallmentLoanCharge();
}
if (chargePerInstallment.isWaived()) {
throw new LoanChargeCannotBePayedException(LOAN_CHARGE_CANNOT_BE_PAYED_REASON.ALREADY_WAIVED, loanCharge.getId());
} else if (chargePerInstallment.isPaid()) { throw new LoanChargeCannotBePayedException(
LOAN_CHARGE_CANNOT_BE_PAYED_REASON.ALREADY_PAID, loanCharge.getId()); }
loanInstallmentNumber = chargePerInstallment.getRepaymentInstallment().getInstallmentNumber();
amount = chargePerInstallment.getAmountOutstanding();
}
final PortfolioAccountData portfolioAccountData = this.accountAssociationsReadPlatformService.retriveLoanLinkedAssociation(loanId);
if (portfolioAccountData == null) {
final String errorMessage = "Charge with id:" + loanChargeId + " requires linked savings account for payment";
throw new LinkedAccountRequiredException("loanCharge.pay", errorMessage, loanChargeId);
}
final SavingsAccount fromSavingsAccount = null;
final boolean isRegularTransaction = true;
final boolean isExceptionForBalanceCheck = false;
final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(transactionDate, amount, PortfolioAccountType.SAVINGS,
PortfolioAccountType.LOAN, portfolioAccountData.accountId(), loanId, "Loan Charge Payment", locale, fmt, null, null,
LoanTransactionType.CHARGE_PAYMENT.getValue(), loanChargeId, loanInstallmentNumber,
AccountTransferType.CHARGE_PAYMENT.getValue(), null, null, null, null, null, fromSavingsAccount, isRegularTransaction,
isExceptionForBalanceCheck);
this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO);
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(loanChargeId) //
.withOfficeId(loan.getOfficeId()) //
.withClientId(loan.getClientId()) //
.withGroupId(loan.getGroupId()) //
.withLoanId(loanId) //
.withSavingsId(portfolioAccountData.accountId()).build();
}
public void disburseLoanToSavings(final Loan loan, final JsonCommand command, final Money amount, final PaymentDetail paymentDetail) {
final LocalDate transactionDate = command.localDateValueOfParameterNamed("actualDisbursementDate");
final String txnExternalId = command.stringValueOfParameterNamedAllowingNull("externalId");
final Locale locale = command.extractLocale();
final DateTimeFormatter fmt = DateTimeFormat.forPattern(command.dateFormat()).withLocale(locale);
final PortfolioAccountData portfolioAccountData = this.accountAssociationsReadPlatformService.retriveLoanLinkedAssociation(loan
.getId());
if (portfolioAccountData == null) {
final String errorMessage = "Disburse Loan with id:" + loan.getId() + " requires linked savings account for payment";
throw new LinkedAccountRequiredException("loan.disburse.to.savings", errorMessage, loan.getId());
}
final SavingsAccount fromSavingsAccount = null;
final boolean isExceptionForBalanceCheck = false;
final boolean isRegularTransaction = true;
final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(transactionDate, amount.getAmount(),
PortfolioAccountType.LOAN, PortfolioAccountType.SAVINGS, loan.getId(), portfolioAccountData.accountId(),
"Loan Disbursement", locale, fmt, paymentDetail, LoanTransactionType.DISBURSEMENT.getValue(), null, null, null,
AccountTransferType.ACCOUNT_TRANSFER.getValue(), null, null, txnExternalId, loan, null, fromSavingsAccount,
isRegularTransaction, isExceptionForBalanceCheck);
this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO);
}
@Override
@CronTarget(jobName = JobName.TRANSFER_FEE_CHARGE_FOR_LOANS)
public void transferFeeCharges() throws JobExecutionException {
final Collection<LoanChargeData> chargeDatas = this.loanChargeReadPlatformService.retrieveLoanChargesForFeePayment(
ChargePaymentMode.ACCOUNT_TRANSFER.getValue(), LoanStatus.ACTIVE.getValue());
final boolean isRegularTransaction = true;
final StringBuilder sb = new StringBuilder();
if (chargeDatas != null) {
for (final LoanChargeData chargeData : chargeDatas) {
if (chargeData.isInstallmentFee()) {
final Collection<LoanInstallmentChargeData> chargePerInstallments = this.loanChargeReadPlatformService
.retrieveInstallmentLoanCharges(chargeData.getId(), true);
PortfolioAccountData portfolioAccountData = null;
for (final LoanInstallmentChargeData installmentChargeData : chargePerInstallments) {
if (!installmentChargeData.getDueDate().isAfter(new LocalDate())) {
if (portfolioAccountData == null) {
portfolioAccountData = this.accountAssociationsReadPlatformService.retriveLoanLinkedAssociation(chargeData
.getLoanId());
}
final SavingsAccount fromSavingsAccount = null;
final boolean isExceptionForBalanceCheck = false;
final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(new LocalDate(),
installmentChargeData.getAmountOutstanding(), PortfolioAccountType.SAVINGS, PortfolioAccountType.LOAN,
portfolioAccountData.accountId(), chargeData.getLoanId(), "Loan Charge Payment", null, null, null,
null, LoanTransactionType.CHARGE_PAYMENT.getValue(), chargeData.getId(),
installmentChargeData.getInstallmentNumber(), AccountTransferType.CHARGE_PAYMENT.getValue(), null,
null, null, null, null, fromSavingsAccount, isRegularTransaction, isExceptionForBalanceCheck);
transferFeeCharge(sb, accountTransferDTO);
}
}
} else if (chargeData.getDueDate() != null && !chargeData.getDueDate().isAfter(new LocalDate())) {
final PortfolioAccountData portfolioAccountData = this.accountAssociationsReadPlatformService
.retriveLoanLinkedAssociation(chargeData.getLoanId());
final SavingsAccount fromSavingsAccount = null;
final boolean isExceptionForBalanceCheck = false;
final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(new LocalDate(),
chargeData.getAmountOutstanding(), PortfolioAccountType.SAVINGS, PortfolioAccountType.LOAN,
portfolioAccountData.accountId(), chargeData.getLoanId(), "Loan Charge Payment", null, null, null, null,
LoanTransactionType.CHARGE_PAYMENT.getValue(), chargeData.getId(), null,
AccountTransferType.CHARGE_PAYMENT.getValue(), null, null, null, null, null, fromSavingsAccount,
isRegularTransaction, isExceptionForBalanceCheck);
transferFeeCharge(sb, accountTransferDTO);
}
}
}
if (sb.length() > 0) { throw new JobExecutionException(sb.toString()); }
}
/**
* @param sb
* @param accountTransferDTO
*/
private void transferFeeCharge(final StringBuilder sb, final AccountTransferDTO accountTransferDTO) {
try {
this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO);
} catch (final PlatformApiDataValidationException e) {
sb.append("Validation exception while paying charge ").append(accountTransferDTO.getChargeId()).append(" for loan id:")
.append(accountTransferDTO.getToAccountId()).append("--------");
} catch (final InsufficientAccountBalanceException e) {
sb.append("InsufficientAccountBalance Exception while paying charge ").append(accountTransferDTO.getChargeId())
.append("for loan id:").append(accountTransferDTO.getToAccountId()).append("--------");
}
}
private LoanCharge retrieveLoanChargeBy(final Long loanId, final Long loanChargeId) {
final LoanCharge loanCharge = this.loanChargeRepository.findOne(loanChargeId);
if (loanCharge == null) { throw new LoanChargeNotFoundException(loanChargeId); }
if (loanCharge.hasNotLoanIdentifiedBy(loanId)) { throw new LoanChargeNotFoundException(loanChargeId, loanId); }
return loanCharge;
}
@Transactional
@Override
public LoanTransaction initiateLoanTransfer(final Long accountId, final LocalDate transferDate) {
AppUser currentUser = getAppUserIfPresent();
final Loan loan = this.loanAssembler.assembleFrom(accountId);
checkClientOrGroupActive(loan);
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_INITIATE_TRANSFER,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
final List<Long> existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds());
final List<Long> existingReversedTransactionIds = new ArrayList<>(loan.findExistingReversedTransactionIds());
final LoanTransaction newTransferTransaction = LoanTransaction.initiateTransfer(loan.getOffice(), loan, transferDate,
DateUtils.getLocalDateTimeOfTenant(), currentUser);
loan.getLoanTransactions().add(newTransferTransaction);
loan.setLoanStatus(LoanStatus.TRANSFER_IN_PROGRESS.getValue());
this.loanTransactionRepository.save(newTransferTransaction);
saveLoanWithDataIntegrityViolationChecks(loan);
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_INITIATE_TRANSFER,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
return newTransferTransaction;
}
@Transactional
@Override
public LoanTransaction acceptLoanTransfer(final Long accountId, final LocalDate transferDate, final Office acceptedInOffice,
final Staff loanOfficer) {
AppUser currentUser = getAppUserIfPresent();
final Loan loan = this.loanAssembler.assembleFrom(accountId);
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_ACCEPT_TRANSFER,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
final List<Long> existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds());
final List<Long> existingReversedTransactionIds = new ArrayList<>(loan.findExistingReversedTransactionIds());
final LoanTransaction newTransferAcceptanceTransaction = LoanTransaction.approveTransfer(acceptedInOffice, loan, transferDate,
DateUtils.getLocalDateTimeOfTenant(), currentUser);
loan.getLoanTransactions().add(newTransferAcceptanceTransaction);
if (loan.getTotalOverpaid() != null) {
loan.setLoanStatus(LoanStatus.OVERPAID.getValue());
} else {
loan.setLoanStatus(LoanStatus.ACTIVE.getValue());
}
if (loanOfficer != null) {
loan.reassignLoanOfficer(loanOfficer, transferDate);
}
this.loanTransactionRepository.save(newTransferAcceptanceTransaction);
saveLoanWithDataIntegrityViolationChecks(loan);
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_ACCEPT_TRANSFER,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
return newTransferAcceptanceTransaction;
}
@Transactional
@Override
public LoanTransaction withdrawLoanTransfer(final Long accountId, final LocalDate transferDate) {
AppUser currentUser = getAppUserIfPresent();
final Loan loan = this.loanAssembler.assembleFrom(accountId);
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_WITHDRAW_TRANSFER,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
final List<Long> existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds());
final List<Long> existingReversedTransactionIds = new ArrayList<>(loan.findExistingReversedTransactionIds());
final LoanTransaction newTransferAcceptanceTransaction = LoanTransaction.withdrawTransfer(loan.getOffice(), loan, transferDate,
DateUtils.getLocalDateTimeOfTenant(), currentUser);
loan.getLoanTransactions().add(newTransferAcceptanceTransaction);
loan.setLoanStatus(LoanStatus.ACTIVE.getValue());
this.loanTransactionRepository.save(newTransferAcceptanceTransaction);
saveLoanWithDataIntegrityViolationChecks(loan);
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_WITHDRAW_TRANSFER,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
return newTransferAcceptanceTransaction;
}
@Transactional
@Override
public void rejectLoanTransfer(final Long accountId) {
final Loan loan = this.loanAssembler.assembleFrom(accountId);
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_REJECT_TRANSFER,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
loan.setLoanStatus(LoanStatus.TRANSFER_ON_HOLD.getValue());
saveLoanWithDataIntegrityViolationChecks(loan);
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_REJECT_TRANSFER,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
}
@Transactional
@Override
public CommandProcessingResult loanReassignment(final Long loanId, final JsonCommand command) {
this.loanEventApiJsonValidator.validateUpdateOfLoanOfficer(command.json());
final Long fromLoanOfficerId = command.longValueOfParameterNamed("fromLoanOfficerId");
final Long toLoanOfficerId = command.longValueOfParameterNamed("toLoanOfficerId");
final Staff fromLoanOfficer = this.loanAssembler.findLoanOfficerByIdIfProvided(fromLoanOfficerId);
final Staff toLoanOfficer = this.loanAssembler.findLoanOfficerByIdIfProvided(toLoanOfficerId);
final LocalDate dateOfLoanOfficerAssignment = command.localDateValueOfParameterNamed("assignmentDate");
final Loan loan = this.loanAssembler.assembleFrom(loanId);
checkClientOrGroupActive(loan);
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_REASSIGN_OFFICER,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
if (!loan.hasLoanOfficer(fromLoanOfficer)) { throw new LoanOfficerAssignmentException(loanId, fromLoanOfficerId); }
loan.reassignLoanOfficer(toLoanOfficer, dateOfLoanOfficerAssignment);
saveLoanWithDataIntegrityViolationChecks(loan);
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_REASSIGN_OFFICER,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(loanId) //
.withOfficeId(loan.getOfficeId()) //
.withClientId(loan.getClientId()) //
.withGroupId(loan.getGroupId()) //
.withLoanId(loanId) //
.build();
}
@Transactional
@Override
public CommandProcessingResult bulkLoanReassignment(final JsonCommand command) {
this.loanEventApiJsonValidator.validateForBulkLoanReassignment(command.json());
final Long fromLoanOfficerId = command.longValueOfParameterNamed("fromLoanOfficerId");
final Long toLoanOfficerId = command.longValueOfParameterNamed("toLoanOfficerId");
final String[] loanIds = command.arrayValueOfParameterNamed("loans");
final LocalDate dateOfLoanOfficerAssignment = command.localDateValueOfParameterNamed("assignmentDate");
final Staff fromLoanOfficer = this.loanAssembler.findLoanOfficerByIdIfProvided(fromLoanOfficerId);
final Staff toLoanOfficer = this.loanAssembler.findLoanOfficerByIdIfProvided(toLoanOfficerId);
for (final String loanIdString : loanIds) {
final Long loanId = Long.valueOf(loanIdString);
final Loan loan = this.loanAssembler.assembleFrom(loanId);
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_REASSIGN_OFFICER,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
checkClientOrGroupActive(loan);
if (!loan.hasLoanOfficer(fromLoanOfficer)) { throw new LoanOfficerAssignmentException(loanId, fromLoanOfficerId); }
loan.reassignLoanOfficer(toLoanOfficer, dateOfLoanOfficerAssignment);
saveLoanWithDataIntegrityViolationChecks(loan);
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_REASSIGN_OFFICER,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
}
this.loanRepository.flush();
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.build();
}
@Transactional
@Override
public CommandProcessingResult removeLoanOfficer(final Long loanId, final JsonCommand command) {
final LoanUpdateCommand loanUpdateCommand = this.loanUpdateCommandFromApiJsonDeserializer.commandFromApiJson(command.json());
loanUpdateCommand.validate();
final LocalDate dateOfLoanOfficerunAssigned = command.localDateValueOfParameterNamed("unassignedDate");
final Loan loan = this.loanAssembler.assembleFrom(loanId);
checkClientOrGroupActive(loan);
if (loan.getLoanOfficer() == null) { throw new LoanOfficerUnassignmentException(loanId); }
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_REMOVE_OFFICER,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
loan.removeLoanOfficer(dateOfLoanOfficerunAssigned);
saveLoanWithDataIntegrityViolationChecks(loan);
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_REMOVE_OFFICER,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(loanId) //
.withOfficeId(loan.getOfficeId()) //
.withClientId(loan.getClientId()) //
.withGroupId(loan.getGroupId()) //
.withLoanId(loanId) //
.build();
}
private void postJournalEntries(final Loan loan, final List<Long> existingTransactionIds,
final List<Long> existingReversedTransactionIds) {
final MonetaryCurrency currency = loan.getCurrency();
final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency);
boolean isAccountTransfer = false;
final Map<String, Object> accountingBridgeData = loan.deriveAccountingBridgeData(applicationCurrency.toData(),
existingTransactionIds, existingReversedTransactionIds, isAccountTransfer);
this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData);
}
@Transactional
@Override
public void applyMeetingDateChanges(final Calendar calendar, final Collection<CalendarInstance> loanCalendarInstances) {
final Boolean reschedulebasedOnMeetingDates = null;
final LocalDate presentMeetingDate = null;
final LocalDate newMeetingDate = null;
applyMeetingDateChanges(calendar, loanCalendarInstances, reschedulebasedOnMeetingDates, presentMeetingDate, newMeetingDate);
}
@Transactional
@Override
public void applyMeetingDateChanges(final Calendar calendar, final Collection<CalendarInstance> loanCalendarInstances,
final Boolean reschedulebasedOnMeetingDates, final LocalDate presentMeetingDate, final LocalDate newMeetingDate) {
final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled();
final WorkingDays workingDays = this.workingDaysRepository.findOne();
final AppUser currentUser = getAppUserIfPresent();
final List<Long> existingTransactionIds = new ArrayList<>();
final List<Long> existingReversedTransactionIds = new ArrayList<>();
final Collection<Integer> loanStatuses = new ArrayList<>(Arrays.asList(LoanStatus.SUBMITTED_AND_PENDING_APPROVAL.getValue(),
LoanStatus.APPROVED.getValue(), LoanStatus.ACTIVE.getValue()));
final Collection<Integer> loanTypes = new ArrayList<>(Arrays.asList(AccountType.GROUP.getValue(), AccountType.JLG.getValue()));
final Collection<Long> loanIds = new ArrayList<>(loanCalendarInstances.size());
// loop through loanCalendarInstances to get loan ids
for (final CalendarInstance calendarInstance : loanCalendarInstances) {
loanIds.add(calendarInstance.getEntityId());
}
final List<Loan> loans = this.loanRepository.findByIdsAndLoanStatusAndLoanType(loanIds, loanStatuses, loanTypes);
List<Holiday> holidays = null;
final LocalDate recalculateFrom = null;
// loop through each loan to reschedule the repayment dates
for (final Loan loan : loans) {
if (loan != null) {
holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(loan.getOfficeId(), loan.getDisbursementDate().toDate());
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
loan.setHelpers(null, this.loanSummaryWrapper, this.transactionProcessingStrategy);
loan.recalculateScheduleFromLastTransaction(scheduleGeneratorDTO, existingTransactionIds,
existingReversedTransactionIds, currentUser);
this.loanScheduleHistoryWritePlatformService.createAndSaveLoanScheduleArchive(
loan.fetchRepaymentScheduleInstallments(), loan, null);
} else if (reschedulebasedOnMeetingDates != null && reschedulebasedOnMeetingDates) {
loan.updateLoanRepaymentScheduleDates(calendar.getStartDateLocalDate(), calendar.getRecurrence(), isHolidayEnabled,
holidays, workingDays, reschedulebasedOnMeetingDates, presentMeetingDate, newMeetingDate);
} else {
loan.updateLoanRepaymentScheduleDates(calendar.getStartDateLocalDate(), calendar.getRecurrence(), isHolidayEnabled,
holidays, workingDays);
}
saveLoanWithDataIntegrityViolationChecks(loan);
}
}
}
private void removeLoanCycle(final Loan loan) {
final List<Loan> loansToUpdate;
if (loan.isGroupLoan()) {
if (loan.loanProduct().isIncludeInBorrowerCycle()) {
loansToUpdate = this.loanRepository.getGroupLoansToUpdateLoanCounter(loan.getCurrentLoanCounter(), loan.getGroupId(),
AccountType.GROUP.getValue());
} else {
loansToUpdate = this.loanRepository.getGroupLoansToUpdateLoanProductCounter(loan.getLoanProductLoanCounter(),
loan.getGroupId(), AccountType.GROUP.getValue());
}
} else {
if (loan.loanProduct().isIncludeInBorrowerCycle()) {
loansToUpdate = this.loanRepository
.getClientOrJLGLoansToUpdateLoanCounter(loan.getCurrentLoanCounter(), loan.getClientId());
} else {
loansToUpdate = this.loanRepository.getClientLoansToUpdateLoanProductCounter(loan.getLoanProductLoanCounter(),
loan.getClientId());
}
}
if (loansToUpdate != null) {
updateLoanCycleCounter(loansToUpdate, loan);
}
loan.updateClientLoanCounter(null);
loan.updateLoanProductLoanCounter(null);
}
private void updateLoanCounters(final Loan loan, final LocalDate actualDisbursementDate) {
if (loan.isGroupLoan()) {
final List<Loan> loansToUpdateForLoanCounter = this.loanRepository.getGroupLoansDisbursedAfter(actualDisbursementDate.toDate(),
loan.getGroupId(), AccountType.GROUP.getValue());
final Integer newLoanCounter = getNewGroupLoanCounter(loan);
final Integer newLoanProductCounter = getNewGroupLoanProductCounter(loan);
updateLoanCounter(loan, loansToUpdateForLoanCounter, newLoanCounter, newLoanProductCounter);
} else {
final List<Loan> loansToUpdateForLoanCounter = this.loanRepository.getClientOrJLGLoansDisbursedAfter(
actualDisbursementDate.toDate(), loan.getClientId());
final Integer newLoanCounter = getNewClientOrJLGLoanCounter(loan);
final Integer newLoanProductCounter = getNewClientOrJLGLoanProductCounter(loan);
updateLoanCounter(loan, loansToUpdateForLoanCounter, newLoanCounter, newLoanProductCounter);
}
}
private Integer getNewGroupLoanCounter(final Loan loan) {
Integer maxClientLoanCounter = this.loanRepository.getMaxGroupLoanCounter(loan.getGroupId(), AccountType.GROUP.getValue());
if (maxClientLoanCounter == null) {
maxClientLoanCounter = 1;
} else {
maxClientLoanCounter = maxClientLoanCounter + 1;
}
return maxClientLoanCounter;
}
private Integer getNewGroupLoanProductCounter(final Loan loan) {
Integer maxLoanProductLoanCounter = this.loanRepository.getMaxGroupLoanProductCounter(loan.loanProduct().getId(),
loan.getGroupId(), AccountType.GROUP.getValue());
if (maxLoanProductLoanCounter == null) {
maxLoanProductLoanCounter = 1;
} else {
maxLoanProductLoanCounter = maxLoanProductLoanCounter + 1;
}
return maxLoanProductLoanCounter;
}
private void updateLoanCounter(final Loan loan, final List<Loan> loansToUpdateForLoanCounter, Integer newLoanCounter,
Integer newLoanProductCounter) {
final boolean includeInBorrowerCycle = loan.loanProduct().isIncludeInBorrowerCycle();
for (final Loan loanToUpdate : loansToUpdateForLoanCounter) {
// Update client loan counter if loan product includeInBorrowerCycle
// is true
if (loanToUpdate.loanProduct().isIncludeInBorrowerCycle()) {
Integer currentLoanCounter = loanToUpdate.getCurrentLoanCounter() == null ? 1 : loanToUpdate.getCurrentLoanCounter();
if (newLoanCounter > currentLoanCounter) {
newLoanCounter = currentLoanCounter;
}
loanToUpdate.updateClientLoanCounter(++currentLoanCounter);
}
if (loanToUpdate.loanProduct().getId().equals(loan.loanProduct().getId())) {
Integer loanProductLoanCounter = loanToUpdate.getLoanProductLoanCounter();
if (newLoanProductCounter > loanProductLoanCounter) {
newLoanProductCounter = loanProductLoanCounter;
}
loanToUpdate.updateLoanProductLoanCounter(++loanProductLoanCounter);
}
}
if (includeInBorrowerCycle) {
loan.updateClientLoanCounter(newLoanCounter);
} else {
loan.updateClientLoanCounter(null);
}
loan.updateLoanProductLoanCounter(newLoanProductCounter);
this.loanRepository.save(loansToUpdateForLoanCounter);
}
private Integer getNewClientOrJLGLoanCounter(final Loan loan) {
Integer maxClientLoanCounter = this.loanRepository.getMaxClientOrJLGLoanCounter(loan.getClientId());
if (maxClientLoanCounter == null) {
maxClientLoanCounter = 1;
} else {
maxClientLoanCounter = maxClientLoanCounter + 1;
}
return maxClientLoanCounter;
}
private Integer getNewClientOrJLGLoanProductCounter(final Loan loan) {
Integer maxLoanProductLoanCounter = this.loanRepository.getMaxClientOrJLGLoanProductCounter(loan.loanProduct().getId(),
loan.getClientId());
if (maxLoanProductLoanCounter == null) {
maxLoanProductLoanCounter = 1;
} else {
maxLoanProductLoanCounter = maxLoanProductLoanCounter + 1;
}
return maxLoanProductLoanCounter;
}
private void updateLoanCycleCounter(final List<Loan> loansToUpdate, final Loan loan) {
final Integer currentLoancounter = loan.getCurrentLoanCounter();
final Integer currentLoanProductCounter = loan.getLoanProductLoanCounter();
for (final Loan loanToUpdate : loansToUpdate) {
if (loan.loanProduct().isIncludeInBorrowerCycle()) {
Integer runningLoancounter = loanToUpdate.getCurrentLoanCounter();
if (runningLoancounter > currentLoancounter) {
loanToUpdate.updateClientLoanCounter(--runningLoancounter);
}
}
if (loan.loanProduct().getId().equals(loanToUpdate.loanProduct().getId())) {
Integer runningLoanProductCounter = loanToUpdate.getLoanProductLoanCounter();
if (runningLoanProductCounter > currentLoanProductCounter) {
loanToUpdate.updateLoanProductLoanCounter(--runningLoanProductCounter);
}
}
}
this.loanRepository.save(loansToUpdate);
}
@Transactional
@Override
@CronTarget(jobName = JobName.APPLY_HOLIDAYS_TO_LOANS)
public void applyHolidaysToLoans() {
final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled();
if (!isHolidayEnabled) { return; }
final Collection<Integer> loanStatuses = new ArrayList<>(Arrays.asList(LoanStatus.SUBMITTED_AND_PENDING_APPROVAL.getValue(),
LoanStatus.APPROVED.getValue(), LoanStatus.ACTIVE.getValue()));
// Get all Holidays which are active and not processed
final List<Holiday> holidays = this.holidayRepository.findUnprocessed();
// Loop through all holidays
for (final Holiday holiday : holidays) {
// All offices to which holiday is applied
final Set<Office> offices = holiday.getOffices();
final Collection<Long> officeIds = new ArrayList<>(offices.size());
for (final Office office : offices) {
officeIds.add(office.getId());
}
// get all loans
final List<Loan> loans = new ArrayList<>();
// get all individual and jlg loans
loans.addAll(this.loanRepository.findByClientOfficeIdsAndLoanStatus(officeIds, loanStatuses));
// FIXME: AA optimize to get all client and group loans belongs to a
// office id
// get all group loans
loans.addAll(this.loanRepository.findByGroupOfficeIdsAndLoanStatus(officeIds, loanStatuses));
for (final Loan loan : loans) {
// apply holiday
loan.applyHolidayToRepaymentScheduleDates(holiday);
}
this.loanRepository.save(loans);
holiday.processed();
}
this.holidayRepository.save(holidays);
}
private void checkForProductMixRestrictions(final Loan loan) {
final List<Long> activeLoansLoanProductIds;
final Long productId = loan.loanProduct().getId();
if (loan.isGroupLoan()) {
activeLoansLoanProductIds = this.loanRepository.findActiveLoansLoanProductIdsByGroup(loan.getGroupId(),
LoanStatus.ACTIVE.getValue());
} else {
activeLoansLoanProductIds = this.loanRepository.findActiveLoansLoanProductIdsByClient(loan.getClientId(),
LoanStatus.ACTIVE.getValue());
}
checkForProductMixRestrictions(activeLoansLoanProductIds, productId, loan.loanProduct().productName());
}
private void checkForProductMixRestrictions(final List<Long> activeLoansLoanProductIds, final Long productId, final String productName) {
if (!CollectionUtils.isEmpty(activeLoansLoanProductIds)) {
final Collection<LoanProductData> restrictedPrdouctsList = this.loanProductReadPlatformService
.retrieveRestrictedProductsForMix(productId);
for (final LoanProductData restrictedProduct : restrictedPrdouctsList) {
if (activeLoansLoanProductIds.contains(restrictedProduct.getId())) { throw new LoanDisbursalException(productName,
restrictedProduct.getName()); }
}
}
}
private void checkClientOrGroupActive(final Loan loan) {
final Client client = loan.client();
if (client != null) {
if (client.isNotActive()) { throw new ClientNotActiveException(client.getId()); }
}
final Group group = loan.group();
if (group != null) {
if (group.isNotActive()) { throw new GroupNotActiveException(group.getId()); }
}
}
@Override
@Transactional
public void applyOverdueChargesForLoan(final Long loanId, Collection<OverdueLoanScheduleData> overdueLoanScheduleDatas) {
Loan loan = null;
final List<Long> existingTransactionIds = new ArrayList<>();
final List<Long> existingReversedTransactionIds = new ArrayList<>();
boolean runInterestRecalculation = false;
LocalDate recalculateFrom = DateUtils.getLocalDateOfTenant();
LocalDate lastChargeDate = null;
for (final OverdueLoanScheduleData overdueInstallment : overdueLoanScheduleDatas) {
final JsonElement parsedCommand = this.fromApiJsonHelper.parse(overdueInstallment.toString());
final JsonCommand command = JsonCommand.from(overdueInstallment.toString(), parsedCommand, this.fromApiJsonHelper, null, null,
null, null, null, loanId, null, null, null, null);
LoanOverdueDTO overdueDTO = applyChargeToOverdueLoanInstallment(loanId, overdueInstallment.getChargeId(),
overdueInstallment.getPeriodNumber(), command, loan, existingTransactionIds, existingReversedTransactionIds);
loan = overdueDTO.getLoan();
runInterestRecalculation = runInterestRecalculation || overdueDTO.isRunInterestRecalculation();
if (recalculateFrom.isAfter(overdueDTO.getRecalculateFrom())) {
recalculateFrom = overdueDTO.getRecalculateFrom();
}
if (lastChargeDate == null || overdueDTO.getLastChargeAppliedDate().isAfter(lastChargeDate)) {
lastChargeDate = overdueDTO.getLastChargeAppliedDate();
}
}
if (loan != null) {
boolean reprocessRequired = true;
LocalDate recalculatedTill = loan.fetchInterestRecalculateFromDate();
if (recalculateFrom.isAfter(recalculatedTill)) {
recalculateFrom = recalculatedTill;
}
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
if (runInterestRecalculation && loan.isFeeCompoundingEnabledForInterestRecalculation()) {
runScheduleRecalculation(loan, recalculateFrom);
reprocessRequired = false;
}
updateOriginalSchedule(loan);
}
if (reprocessRequired) {
addInstallmentIfPenaltyAppliedAfterLastDueDate(loan, lastChargeDate);
ChangedTransactionDetail changedTransactionDetail = loan.reprocessTransactions();
if (changedTransactionDetail != null) {
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
this.loanTransactionRepository.save(mapEntry.getValue());
// update loan with references to the newly created
// transactions
loan.getLoanTransactions().add(mapEntry.getValue());
this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
}
}
saveLoanWithDataIntegrityViolationChecks(loan);
}
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled() && runInterestRecalculation
&& loan.isFeeCompoundingEnabledForInterestRecalculation()) {
this.loanAccountDomainService.recalculateAccruals(loan);
}
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_APPLY_OVERDUE_CHARGE,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
}
}
private void addInstallmentIfPenaltyAppliedAfterLastDueDate(Loan loan, LocalDate lastChargeDate) {
if (lastChargeDate != null) {
List<LoanRepaymentScheduleInstallment> installments = loan.fetchRepaymentScheduleInstallments();
LoanRepaymentScheduleInstallment lastInstallment = loan.fetchRepaymentScheduleInstallment(installments.size());
if (lastChargeDate.isAfter(lastInstallment.getDueDate())) {
if (lastInstallment.isRecalculatedInterestComponent()) {
installments.remove(lastInstallment);
lastInstallment = loan.fetchRepaymentScheduleInstallment(installments.size());
}
boolean recalculatedInterestComponent = true;
BigDecimal principal = BigDecimal.ZERO;
BigDecimal interest = BigDecimal.ZERO;
BigDecimal feeCharges = BigDecimal.ZERO;
BigDecimal penaltyCharges = BigDecimal.ONE;
LoanRepaymentScheduleInstallment newEntry = new LoanRepaymentScheduleInstallment(loan, installments.size() + 1,
lastInstallment.getDueDate(), lastChargeDate, principal, interest, feeCharges, penaltyCharges,
recalculatedInterestComponent);
installments.add(newEntry);
}
}
}
public LoanOverdueDTO applyChargeToOverdueLoanInstallment(final Long loanId, final Long loanChargeId, final Integer periodNumber,
final JsonCommand command, Loan loan, final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds) {
boolean runInterestRecalculation = false;
final Charge chargeDefinition = this.chargeRepository.findOneWithNotFoundDetection(loanChargeId);
Collection<Integer> frequencyNumbers = loanChargeReadPlatformService.retrieveOverdueInstallmentChargeFrequencyNumber(loanId,
chargeDefinition.getId(), periodNumber);
Integer feeFrequency = chargeDefinition.feeFrequency();
final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator();
Map<Integer, LocalDate> scheduleDates = new HashMap<>();
final Long penaltyWaitPeriodValue = this.configurationDomainService.retrievePenaltyWaitPeriod();
final Long penaltyPostingWaitPeriodValue = this.configurationDomainService.retrieveGraceOnPenaltyPostingPeriod();
final LocalDate dueDate = command.localDateValueOfParameterNamed("dueDate");
Long diff = penaltyWaitPeriodValue + 1 - penaltyPostingWaitPeriodValue;
if (diff < 0) {
diff = 0L;
}
LocalDate startDate = dueDate.plusDays(penaltyWaitPeriodValue.intValue() + 1);
Integer frequencyNunber = 1;
if (feeFrequency == null) {
scheduleDates.put(frequencyNunber++, startDate.minusDays(diff.intValue()));
} else {
while (DateUtils.getLocalDateOfTenant().isAfter(startDate)) {
scheduleDates.put(frequencyNunber++, startDate.minusDays(diff.intValue()));
LocalDate scheduleDate = scheduledDateGenerator.getRepaymentPeriodDate(PeriodFrequencyType.fromInt(feeFrequency),
chargeDefinition.feeInterval(), startDate, null, null);
startDate = scheduleDate;
}
}
for (Integer frequency : frequencyNumbers) {
scheduleDates.remove(frequency);
}
LoanRepaymentScheduleInstallment installment = null;
LocalDate lastChargeAppliedDate = dueDate;
if (!scheduleDates.isEmpty()) {
if (loan == null) {
loan = this.loanAssembler.assembleFrom(loanId);
checkClientOrGroupActive(loan);
existingTransactionIds.addAll(loan.findExistingTransactionIds());
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
}
installment = loan.fetchRepaymentScheduleInstallment(periodNumber);
lastChargeAppliedDate = installment.getDueDate();
}
LocalDate recalculateFrom = DateUtils.getLocalDateOfTenant();
if (loan != null) {
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_APPLY_OVERDUE_CHARGE,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
for (Map.Entry<Integer, LocalDate> entry : scheduleDates.entrySet()) {
final LoanCharge loanCharge = LoanCharge.createNewFromJson(loan, chargeDefinition, command, entry.getValue());
LoanOverdueInstallmentCharge overdueInstallmentCharge = new LoanOverdueInstallmentCharge(loanCharge, installment,
entry.getKey());
loanCharge.updateOverdueInstallmentCharge(overdueInstallmentCharge);
boolean isAppliedOnBackDate = addCharge(loan, chargeDefinition, loanCharge);
runInterestRecalculation = runInterestRecalculation || isAppliedOnBackDate;
if (entry.getValue().isBefore(recalculateFrom)) {
recalculateFrom = entry.getValue();
}
if (entry.getValue().isAfter(lastChargeAppliedDate)) {
lastChargeAppliedDate = entry.getValue();
}
}
}
return new LoanOverdueDTO(loan, runInterestRecalculation, recalculateFrom, lastChargeAppliedDate);
}
@Override
public CommandProcessingResult undoWriteOff(Long loanId) {
final AppUser currentUser = getAppUserIfPresent();
final Loan loan = this.loanAssembler.assembleFrom(loanId);
checkClientOrGroupActive(loan);
final List<Long> existingTransactionIds = new ArrayList<>();
final List<Long> existingReversedTransactionIds = new ArrayList<>();
if (!loan.isClosedWrittenOff()) { throw new PlatformServiceUnavailableException(
"error.msg.loan.status.not.written.off.update.not.allowed", "Loan :" + loanId
+ " update not allowed as loan status is not written off", loanId); }
LocalDate recalculateFrom = null;
LoanTransaction writeOffTransaction = loan.findWriteOffTransaction();
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_UNDO_WRITTEN_OFF,
constructEntityMap(BUSINESS_ENTITY.LOAN_TRANSACTION, writeOffTransaction));
ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
ChangedTransactionDetail changedTransactionDetail = loan.undoWrittenOff(existingTransactionIds, existingReversedTransactionIds,
scheduleGeneratorDTO, currentUser);
if (changedTransactionDetail != null) {
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
this.loanTransactionRepository.save(mapEntry.getValue());
this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
}
}
saveLoanWithDataIntegrityViolationChecks(loan);
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
this.loanAccountDomainService.recalculateAccruals(loan);
if (writeOffTransaction != null) {
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_UNDO_WRITTEN_OFF,
constructEntityMap(BUSINESS_ENTITY.LOAN_TRANSACTION, writeOffTransaction));
}
return new CommandProcessingResultBuilder() //
.withOfficeId(loan.getOfficeId()) //
.withClientId(loan.getClientId()) //
.withGroupId(loan.getGroupId()) //
.withLoanId(loanId) //
.build();
}
private void validateMultiDisbursementData(final JsonCommand command, LocalDate expectedDisbursementDate) {
final String json = command.json();
final JsonElement element = this.fromApiJsonHelper.parse(json);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan");
final JsonArray disbursementDataArray = command.arrayOfParameterNamed(LoanApiConstants.disbursementDataParameterName);
if (disbursementDataArray == null || disbursementDataArray.size() == 0) {
final String errorMessage = "For this loan product, disbursement details must be provided";
throw new MultiDisbursementDataRequiredException(LoanApiConstants.disbursementDataParameterName, errorMessage);
}
final BigDecimal principal = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("approvedLoanAmount", element);
loanApplicationCommandFromApiJsonHelper.validateLoanMultiDisbursementdate(element, baseDataValidator, expectedDisbursementDate,
principal);
if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); }
}
private void validateForAddAndDeleteTranche(final Loan loan) {
BigDecimal totalDisbursedAmount = BigDecimal.ZERO;
Collection<LoanDisbursementDetails> loanDisburseDetails = loan.getDisbursementDetails();
for (LoanDisbursementDetails disbursementDetails : loanDisburseDetails) {
if (disbursementDetails.actualDisbursementDate() != null) {
totalDisbursedAmount = totalDisbursedAmount.add(disbursementDetails.principal());
}
}
if (totalDisbursedAmount.compareTo(loan.getApprovedPrincipal()) == 0) {
final String errorMessage = "loan.disbursement.cannot.be.a.edited";
throw new LoanMultiDisbursementException(errorMessage);
}
}
@Override
@Transactional
public CommandProcessingResult addAndDeleteLoanDisburseDetails(Long loanId, JsonCommand command) {
final Loan loan = this.loanAssembler.assembleFrom(loanId);
checkClientOrGroupActive(loan);
final Map<String, Object> actualChanges = new LinkedHashMap<>();
LocalDate expectedDisbursementDate = loan.getExpectedDisbursedOnLocalDate();
if (!loan.loanProduct().isMultiDisburseLoan()) {
final String errorMessage = "loan.product.does.not.support.multiple.disbursals";
throw new LoanMultiDisbursementException(errorMessage);
}
if (loan.isSubmittedAndPendingApproval() || loan.isClosed() || loan.isClosedWrittenOff() || loan.status().isClosedObligationsMet()
|| loan.status().isOverpaid()) {
final String errorMessage = "cannot.modify.tranches.if.loan.is.pendingapproval.closed.overpaid.writtenoff";
throw new LoanMultiDisbursementException(errorMessage);
}
validateMultiDisbursementData(command, expectedDisbursementDate);
this.validateForAddAndDeleteTranche(loan);
loan.updateDisbursementDetails(command, actualChanges);
if (loan.getDisbursementDetails().isEmpty()) {
final String errorMessage = "For this loan product, disbursement details must be provided";
throw new MultiDisbursementDataRequiredException(LoanApiConstants.disbursementDataParameterName, errorMessage);
}
if (loan.getDisbursementDetails().size() > loan.loanProduct().maxTrancheCount()) {
final String errorMessage = "Number of tranche shouldn't be greter than " + loan.loanProduct().maxTrancheCount();
throw new ExceedingTrancheCountException(LoanApiConstants.disbursementDataParameterName, errorMessage, loan.loanProduct()
.maxTrancheCount(), loan.getDisbursementDetails().size());
}
LoanDisbursementDetails updateDetails = null;
return processLoanDisbursementDetail(loan, loanId, command, updateDetails);
}
private CommandProcessingResult processLoanDisbursementDetail(final Loan loan, Long loanId, JsonCommand command,
LoanDisbursementDetails loanDisbursementDetails) {
final List<Long> existingTransactionIds = new ArrayList<>();
final List<Long> existingReversedTransactionIds = new ArrayList<>();
existingTransactionIds.addAll(loan.findExistingTransactionIds());
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
final Map<String, Object> changes = new LinkedHashMap<>();
LocalDate recalculateFrom = null;
ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
ChangedTransactionDetail changedTransactionDetail = null;
AppUser currentUser = getAppUserIfPresent();
if (command.entityId() != null) {
changedTransactionDetail = loan.updateDisbursementDateAndAmountForTranche(loanDisbursementDetails, command,
changes, scheduleGeneratorDTO, currentUser);
} else {
// BigDecimal setAmount = loan.getApprovedPrincipal();
Collection<LoanDisbursementDetails> loanDisburseDetails = loan.getDisbursementDetails();
BigDecimal setAmount = BigDecimal.ZERO;
for (LoanDisbursementDetails details : loanDisburseDetails) {
if (details.expectedDisbursementDate() != null) {
setAmount = setAmount.add(details.principal());
}
}
loan.repaymentScheduleDetail().setPrincipal(setAmount);
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
loan.regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO, currentUser);
} else {
loan.regenerateRepaymentSchedule(scheduleGeneratorDTO, currentUser);
loan.processPostDisbursementTransactions();
}
}
saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
if (command.entityId() != null && changedTransactionDetail != null) {
for (Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
}
}
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
createLoanScheduleArchive(loan, scheduleGeneratorDTO);
}
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
this.loanAccountDomainService.recalculateAccruals(loan);
return new CommandProcessingResultBuilder() //
.withOfficeId(loan.getOfficeId()) //
.withClientId(loan.getClientId()) //
.withGroupId(loan.getGroupId()) //
.withLoanId(loanId) //
.with(changes).build();
}
@Override
@Transactional
public CommandProcessingResult updateDisbursementDateAndAmountForTranche(final Long loanId, final Long disbursementId,
final JsonCommand command) {
final Loan loan = this.loanAssembler.assembleFrom(loanId);
checkClientOrGroupActive(loan);
LoanDisbursementDetails loanDisbursementDetails = loan.fetchLoanDisbursementsById(disbursementId);
this.loanEventApiJsonValidator.validateUpdateDisbursementDateAndAmount(command.json(), loanDisbursementDetails);
return processLoanDisbursementDetail(loan, loanId, command, loanDisbursementDetails);
}
public LoanTransaction disburseLoanAmountToSavings(final Long loanId, Long loanChargeId, final JsonCommand command,
final boolean isChargeIdIncludedInJson) {
LoanTransaction transaction = null;
this.loanEventApiJsonValidator.validateChargePaymentTransaction(command.json(), isChargeIdIncludedInJson);
if (isChargeIdIncludedInJson) {
loanChargeId = command.longValueOfParameterNamed("chargeId");
}
final Loan loan = this.loanAssembler.assembleFrom(loanId);
checkClientOrGroupActive(loan);
final LoanCharge loanCharge = retrieveLoanChargeBy(loanId, loanChargeId);
// Charges may be waived only when the loan associated with them are
// active
if (!loan.status().isActive()) { throw new LoanChargeCannotBePayedException(LOAN_CHARGE_CANNOT_BE_PAYED_REASON.LOAN_INACTIVE,
loanCharge.getId()); }
// validate loan charge is not already paid or waived
if (loanCharge.isWaived()) {
throw new LoanChargeCannotBePayedException(LOAN_CHARGE_CANNOT_BE_PAYED_REASON.ALREADY_WAIVED, loanCharge.getId());
} else if (loanCharge.isPaid()) { throw new LoanChargeCannotBePayedException(LOAN_CHARGE_CANNOT_BE_PAYED_REASON.ALREADY_PAID,
loanCharge.getId()); }
if (!loanCharge.getChargePaymentMode().isPaymentModeAccountTransfer()) { throw new LoanChargeCannotBePayedException(
LOAN_CHARGE_CANNOT_BE_PAYED_REASON.CHARGE_NOT_ACCOUNT_TRANSFER, loanCharge.getId()); }
final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
final Locale locale = command.extractLocale();
final DateTimeFormatter fmt = DateTimeFormat.forPattern(command.dateFormat()).withLocale(locale);
Integer loanInstallmentNumber = null;
BigDecimal amount = loanCharge.amountOutstanding();
if (loanCharge.isInstalmentFee()) {
LoanInstallmentCharge chargePerInstallment = null;
final LocalDate dueDate = command.localDateValueOfParameterNamed("dueDate");
final Integer installmentNumber = command.integerValueOfParameterNamed("installmentNumber");
if (dueDate != null) {
chargePerInstallment = loanCharge.getInstallmentLoanCharge(dueDate);
} else if (installmentNumber != null) {
chargePerInstallment = loanCharge.getInstallmentLoanCharge(installmentNumber);
}
if (chargePerInstallment == null) {
chargePerInstallment = loanCharge.getUnpaidInstallmentLoanCharge();
}
if (chargePerInstallment.isWaived()) {
throw new LoanChargeCannotBePayedException(LOAN_CHARGE_CANNOT_BE_PAYED_REASON.ALREADY_WAIVED, loanCharge.getId());
} else if (chargePerInstallment.isPaid()) { throw new LoanChargeCannotBePayedException(
LOAN_CHARGE_CANNOT_BE_PAYED_REASON.ALREADY_PAID, loanCharge.getId()); }
loanInstallmentNumber = chargePerInstallment.getRepaymentInstallment().getInstallmentNumber();
amount = chargePerInstallment.getAmountOutstanding();
}
final PortfolioAccountData portfolioAccountData = this.accountAssociationsReadPlatformService.retriveLoanLinkedAssociation(loanId);
if (portfolioAccountData == null) {
final String errorMessage = "Charge with id:" + loanChargeId + " requires linked savings account for payment";
throw new LinkedAccountRequiredException("loanCharge.pay", errorMessage, loanChargeId);
}
final SavingsAccount fromSavingsAccount = null;
final boolean isRegularTransaction = true;
final boolean isExceptionForBalanceCheck = false;
final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(transactionDate, amount, PortfolioAccountType.SAVINGS,
PortfolioAccountType.LOAN, portfolioAccountData.accountId(), loanId, "Loan Charge Payment", locale, fmt, null, null,
LoanTransactionType.CHARGE_PAYMENT.getValue(), loanChargeId, loanInstallmentNumber,
AccountTransferType.CHARGE_PAYMENT.getValue(), null, null, null, null, null, fromSavingsAccount, isRegularTransaction,
isExceptionForBalanceCheck);
this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO);
return transaction;
}
@Transactional
@Override
public void recalculateInterest(final long loanId) {
Loan loan = this.loanAssembler.assembleFrom(loanId);
LocalDate recalculateFrom = loan.fetchInterestRecalculateFromDate();
AppUser currentUser = getAppUserIfPresent();
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_INTEREST_RECALCULATION,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
final List<Long> existingTransactionIds = new ArrayList<>();
final List<Long> existingReversedTransactionIds = new ArrayList<>();
ScheduleGeneratorDTO generatorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
ChangedTransactionDetail changedTransactionDetail = loan.recalculateScheduleFromLastTransaction(generatorDTO,
existingTransactionIds, existingReversedTransactionIds, currentUser);
saveLoanWithDataIntegrityViolationChecks(loan);
if (changedTransactionDetail != null) {
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
this.loanTransactionRepository.save(mapEntry.getValue());
// update loan with references to the newly created
// transactions
loan.getLoanTransactions().add(mapEntry.getValue());
this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
}
}
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
this.loanAccountDomainService.recalculateAccruals(loan);
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_INTEREST_RECALCULATION,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
}
@Override
public CommandProcessingResult recoverFromGuarantor(final Long loanId) {
final Loan loan = this.loanAssembler.assembleFrom(loanId);
this.guarantorDomainService.transaferFundsFromGuarantor(loan);
return new CommandProcessingResultBuilder().withLoanId(loanId).build();
}
private void updateLoanTransaction(final Long loanTransactionId, final LoanTransaction newLoanTransaction) {
final AccountTransferTransaction transferTransaction = this.accountTransferRepository.findByToLoanTransactionId(loanTransactionId);
if (transferTransaction != null) {
transferTransaction.updateToLoanTransaction(newLoanTransaction);
this.accountTransferRepository.save(transferTransaction);
}
}
private void createLoanScheduleArchive(final Loan loan, final ScheduleGeneratorDTO scheduleGeneratorDTO) {
LoanScheduleModel loanScheduleModel = loan.regenerateScheduleModel(scheduleGeneratorDTO);
List<LoanRepaymentScheduleInstallment> installments = retrieveRepaymentScheduleFromModel(loanScheduleModel);
this.loanScheduleHistoryWritePlatformService.createAndSaveLoanScheduleArchive(installments, loan, null);
}
private void regenerateScheduleOnDisbursement(final JsonCommand command, final Loan loan, final boolean recalculateSchedule,
final ScheduleGeneratorDTO scheduleGeneratorDTO, final LocalDate nextPossibleRepaymentDate, final Date rescheduledRepaymentDate) {
AppUser currentUser = getAppUserIfPresent();
final LocalDate actualDisbursementDate = command.localDateValueOfParameterNamed("actualDisbursementDate");
BigDecimal emiAmount = command.bigDecimalValueOfParameterNamed(LoanApiConstants.emiAmountParameterName);
loan.regenerateScheduleOnDisbursement(scheduleGeneratorDTO, recalculateSchedule, actualDisbursementDate, emiAmount, currentUser,
nextPossibleRepaymentDate, rescheduledRepaymentDate);
}
private List<LoanRepaymentScheduleInstallment> retrieveRepaymentScheduleFromModel(LoanScheduleModel model) {
final List<LoanRepaymentScheduleInstallment> installments = new ArrayList<>();
for (final LoanScheduleModelPeriod scheduledLoanInstallment : model.getPeriods()) {
if (scheduledLoanInstallment.isRepaymentPeriod()) {
final LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(null,
scheduledLoanInstallment.periodNumber(), scheduledLoanInstallment.periodFromDate(),
scheduledLoanInstallment.periodDueDate(), scheduledLoanInstallment.principalDue(),
scheduledLoanInstallment.interestDue(), scheduledLoanInstallment.feeChargesDue(),
scheduledLoanInstallment.penaltyChargesDue(), scheduledLoanInstallment.isRecalculatedInterestComponent());
installments.add(installment);
}
}
return installments;
}
@Override
@Transactional
public CommandProcessingResult makeLoanRefund(Long loanId, JsonCommand command) {
// TODO Auto-generated method stub
this.loanEventApiJsonValidator.validateNewRefundTransaction(command.json());
final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
// checkRefundDateIsAfterAtLeastOneRepayment(loanId, transactionDate);
final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount");
checkIfLoanIsPaidInAdvance(loanId, transactionAmount);
final Map<String, Object> changes = new LinkedHashMap<>();
changes.put("transactionDate", command.stringValueOfParameterNamed("transactionDate"));
changes.put("transactionAmount", command.stringValueOfParameterNamed("transactionAmount"));
changes.put("locale", command.locale());
changes.put("dateFormat", command.dateFormat());
final String noteText = command.stringValueOfParameterNamed("note");
if (StringUtils.isNotBlank(noteText)) {
changes.put("note", noteText);
}
final PaymentDetail paymentDetail = null;
final CommandProcessingResultBuilder commandProcessingResultBuilder = new CommandProcessingResultBuilder();
this.loanAccountDomainService.makeRefundForActiveLoan(loanId, commandProcessingResultBuilder, transactionDate, transactionAmount,
paymentDetail, noteText, null);
return commandProcessingResultBuilder.withCommandId(command.commandId()) //
.withLoanId(loanId) //
.with(changes) //
.build();
}
private void checkIfLoanIsPaidInAdvance(final Long loanId, final BigDecimal transactionAmount) {
BigDecimal overpaid = this.loanReadPlatformService.retrieveTotalPaidInAdvance(loanId).getPaidInAdvance();
if (overpaid == null || overpaid.equals(new BigDecimal(0)) || transactionAmount.floatValue() > overpaid.floatValue()) {
if (overpaid == null) overpaid = BigDecimal.ZERO;
throw new InvalidPaidInAdvanceAmountException(overpaid.toPlainString());
}
}
private AppUser getAppUserIfPresent() {
AppUser user = null;
if (this.context != null) {
user = this.context.getAuthenticatedUserIfPresent();
}
return user;
}
private Map<BUSINESS_ENTITY, Object> constructEntityMap(final BUSINESS_ENTITY entityEvent, Object entity) {
Map<BUSINESS_ENTITY, Object> map = new HashMap<>(1);
map.put(entityEvent, entity);
return map;
}
@Override
@Transactional
public CommandProcessingResult undoLastLoanDisbursal(Long loanId, JsonCommand command) {
final AppUser currentUser = getAppUserIfPresent();
final Loan loan = this.loanAssembler.assembleFrom(loanId);
final LocalDate recalculateFromDate = loan.getLastRepaymentDate();
validateIsMultiDisbursalLoanAndDisbursedMoreThanOneTranche(loan);
checkClientOrGroupActive(loan);
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_UNDO_LASTDISBURSAL,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
final MonetaryCurrency currency = loan.getCurrency();
final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency);
final List<Long> existingTransactionIds = new ArrayList<>();
final List<Long> existingReversedTransactionIds = new ArrayList<>();
ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFromDate);
final Map<String, Object> changes = loan.undoLastDisbursal(scheduleGeneratorDTO, existingTransactionIds,
existingReversedTransactionIds, currentUser, loan);
if (!changes.isEmpty()) {
saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
String noteText = null;
if (command.hasParameter("note")) {
noteText = command.stringValueOfParameterNamed("note");
if (StringUtils.isNotBlank(noteText)) {
final Note note = Note.loanNote(loan, noteText);
this.noteRepository.save(note);
}
}
boolean isAccountTransfer = false;
final Map<String, Object> accountingBridgeData = loan.deriveAccountingBridgeData(applicationCurrency.toData(),
existingTransactionIds, existingReversedTransactionIds, isAccountTransfer);
this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData);
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_UNDO_LASTDISBURSAL,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
}
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(loan.getId()) //
.withOfficeId(loan.getOfficeId()) //
.withClientId(loan.getClientId()) //
.withGroupId(loan.getGroupId()) //
.withLoanId(loanId) //
.with(changes) //
.build();
}
private void validateIsMultiDisbursalLoanAndDisbursedMoreThanOneTranche(Loan loan) {
if (!loan.isMultiDisburmentLoan()) {
final String errorMessage = "loan.product.does.not.support.multiple.disbursals.cannot.undo.last.disbursal";
throw new LoanMultiDisbursementException(errorMessage);
}
Integer trancheDisbursedCount = 0;
for (LoanDisbursementDetails disbursementDetails : loan.getDisbursementDetails()) {
if (disbursementDetails.actualDisbursementDate() != null) {
trancheDisbursedCount++;
}
}
if (trancheDisbursedCount <= 1) {
final String errorMessage = "tranches.should.be.disbursed.more.than.one.to.undo.last.disbursal";
throw new LoanMultiDisbursementException(errorMessage);
}
}
}