| /** |
| * 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 com.google.gson.JsonElement; |
| import com.google.gson.JsonObject; |
| import java.math.BigDecimal; |
| import java.time.LocalDate; |
| import java.time.format.DateTimeFormatter; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| 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 lombok.RequiredArgsConstructor; |
| import lombok.extern.slf4j.Slf4j; |
| import org.apache.commons.lang3.StringUtils; |
| import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService; |
| import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; |
| import org.apache.fineract.infrastructure.core.api.JsonCommand; |
| import org.apache.fineract.infrastructure.core.data.ApiParameterError; |
| import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; |
| import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; |
| import org.apache.fineract.infrastructure.core.domain.ExternalId; |
| import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException; |
| import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; |
| import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; |
| import org.apache.fineract.infrastructure.core.service.DateUtils; |
| import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; |
| import org.apache.fineract.infrastructure.event.business.domain.loan.LoanApplyOverdueChargeBusinessEvent; |
| import org.apache.fineract.infrastructure.event.business.domain.loan.LoanBalanceChangedBusinessEvent; |
| import org.apache.fineract.infrastructure.event.business.domain.loan.charge.LoanAddChargeBusinessEvent; |
| import org.apache.fineract.infrastructure.event.business.domain.loan.charge.LoanDeleteChargeBusinessEvent; |
| import org.apache.fineract.infrastructure.event.business.domain.loan.charge.LoanUpdateChargeBusinessEvent; |
| import org.apache.fineract.infrastructure.event.business.domain.loan.charge.LoanWaiveChargeBusinessEvent; |
| import org.apache.fineract.infrastructure.event.business.domain.loan.charge.LoanWaiveChargeUndoBusinessEvent; |
| import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanChargeAdjustmentPostBusinessEvent; |
| import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanChargeAdjustmentPreBusinessEvent; |
| import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanChargeRefundBusinessEvent; |
| import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; |
| import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; |
| import org.apache.fineract.organisation.monetary.domain.Money; |
| 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.AccountTransferDetailRepository; |
| import org.apache.fineract.portfolio.account.domain.AccountTransferDetails; |
| import org.apache.fineract.portfolio.account.domain.AccountTransferType; |
| import org.apache.fineract.portfolio.account.exception.AccountTransferNotFoundException; |
| import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlatformService; |
| import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService; |
| import org.apache.fineract.portfolio.accountdetails.domain.AccountType; |
| import org.apache.fineract.portfolio.charge.domain.Charge; |
| import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper; |
| import org.apache.fineract.portfolio.charge.exception.ChargeCannotBeAppliedToException; |
| 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.LoanChargeWaiveCannotBeReversedException; |
| import org.apache.fineract.portfolio.client.domain.Client; |
| import org.apache.fineract.portfolio.client.exception.ClientNotActiveException; |
| import org.apache.fineract.portfolio.collateralmanagement.domain.ClientCollateralManagement; |
| import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; |
| 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.data.LoanChargePaidByData; |
| import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO; |
| import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail; |
| 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.LoanChargePaidBy; |
| import org.apache.fineract.portfolio.loanaccount.domain.LoanChargeRepository; |
| import org.apache.fineract.portfolio.loanaccount.domain.LoanCollateralManagement; |
| 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.LoanInterestRecalcualtionAdditionalDetails; |
| 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.LoanRepaymentScheduleTransactionProcessorFactory; |
| import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; |
| import org.apache.fineract.portfolio.loanaccount.domain.LoanTrancheDisbursementCharge; |
| import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; |
| import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation; |
| import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum; |
| import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; |
| import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; |
| import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; |
| import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor; |
| import org.apache.fineract.portfolio.loanaccount.exception.InstallmentNotFoundException; |
| import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException; |
| import org.apache.fineract.portfolio.loanaccount.exception.LoanChargeAdjustmentException; |
| import org.apache.fineract.portfolio.loanaccount.exception.LoanChargeRefundException; |
| import org.apache.fineract.portfolio.loanaccount.exception.LoanTransactionNotFoundException; |
| 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.ScheduledDateGenerator; |
| import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeApiJsonValidator; |
| import org.apache.fineract.portfolio.loanproduct.data.LoanOverdueDTO; |
| import org.apache.fineract.portfolio.loanproduct.exception.InvalidCurrencyException; |
| import org.apache.fineract.portfolio.loanproduct.exception.LinkedAccountRequiredException; |
| 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.springframework.transaction.annotation.Transactional; |
| |
| @Slf4j |
| @RequiredArgsConstructor |
| public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatformService { |
| |
| private static final String AMOUNT = "amount"; |
| private final LoanChargeApiJsonValidator loanChargeApiJsonValidator; |
| private final LoanAssembler loanAssembler; |
| private final ChargeRepositoryWrapper chargeRepository; |
| private final BusinessEventNotifierService businessEventNotifierService; |
| private final LoanTransactionRepository loanTransactionRepository; |
| private final AccountTransfersWritePlatformService accountTransfersWritePlatformService; |
| private final LoanRepositoryWrapper loanRepositoryWrapper; |
| private final JournalEntryWritePlatformService journalEntryWritePlatformService; |
| private final LoanAccountDomainService loanAccountDomainService; |
| private final LoanChargeRepository loanChargeRepository; |
| private final LoanWritePlatformService loanWritePlatformService; |
| private final LoanUtilService loanUtilService; |
| private final LoanChargeReadPlatformService loanChargeReadPlatformService; |
| private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine; |
| private final AccountAssociationsReadPlatformService accountAssociationsReadPlatformService; |
| private final FromJsonHelper fromApiJsonHelper; |
| private final ConfigurationDomainService configurationDomainService; |
| private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory; |
| private final ExternalIdFactory externalIdFactory; |
| private final AccountTransferDetailRepository accountTransferDetailRepository; |
| private final LoanChargeAssembler loanChargeAssembler; |
| private final ReplayedTransactionBusinessEventService replayedTransactionBusinessEventService; |
| private final PaymentDetailWritePlatformService paymentDetailWritePlatformService; |
| private final NoteRepository noteRepository; |
| private final LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService; |
| |
| private static boolean isPartOfThisInstallment(LoanCharge loanCharge, LoanRepaymentScheduleInstallment e) { |
| return DateUtils.isAfter(loanCharge.getDueDate(), e.getFromDate()) && !DateUtils.isAfter(loanCharge.getDueDate(), e.getDueDate()); |
| } |
| |
| @Transactional |
| @Override |
| public CommandProcessingResult addLoanCharge(final Long loanId, final JsonCommand command) { |
| this.loanChargeApiJsonValidator.validateAddLoanCharge(command.json()); |
| |
| Loan loan = this.loanAssembler.assembleFrom(loanId); |
| checkClientOrGroupActive(loan); |
| if (loan.isChargedOff()) { |
| throw new GeneralPlatformDomainRuleException("error.msg.loan.is.charged.off", |
| "Adding charge to Loan: " + loanId + " is not allowed. Loan Account is Charged-off", loanId); |
| } |
| List<LoanDisbursementDetails> loanDisburseDetails = loan.getDisbursementDetails(); |
| final Long chargeDefinitionId = command.longValueOfParameterNamed("chargeId"); |
| final Charge chargeDefinition = this.chargeRepository.findOneWithNotFoundDetection(chargeDefinitionId); |
| |
| if (loan.isDisbursed() && chargeDefinition.isDisbursementCharge()) { |
| // validates whether any pending disbursements are available to |
| // apply this charge |
| validateAddingNewChargeAllowed(loanDisburseDetails); |
| } |
| 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; |
| ExternalId externalId = externalIdFactory.createFromCommand(command, "externalId"); |
| boolean needToGenerateNewExternalId = false; |
| for (LoanDisbursementDetails disbursementDetail : loanDisburseDetails) { |
| if (disbursementDetail.actualDisbursementDate() == null) { |
| // If multiple charges to be applied, only the first one will get the provided externalId, for the |
| // rest we generate new ones (if needed) |
| if (needToGenerateNewExternalId) { |
| externalId = externalIdFactory.create(); |
| } |
| loanCharge = loanChargeAssembler.createNewWithoutLoan(chargeDefinition, disbursementDetail.principal(), null, null, |
| null, disbursementDetail.expectedDisbursementDateAsLocalDate(), null, null, externalId); |
| loanTrancheDisbursementCharge = new LoanTrancheDisbursementCharge(loanCharge, disbursementDetail); |
| loanCharge.updateLoanTrancheDisbursementCharge(loanTrancheDisbursementCharge); |
| businessEventNotifierService.notifyPreBusinessEvent(new LoanAddChargeBusinessEvent(loanCharge)); |
| validateAddLoanCharge(loan, chargeDefinition, loanCharge); |
| addCharge(loan, chargeDefinition, loanCharge); |
| isAppliedOnBackDate = true; |
| if (DateUtils.isAfter(recalculateFrom, disbursementDetail.expectedDisbursementDateAsLocalDate())) { |
| recalculateFrom = disbursementDetail.expectedDisbursementDateAsLocalDate(); |
| } |
| needToGenerateNewExternalId = true; |
| } |
| } |
| if (loanCharge == null) { |
| final String errorMessage = "Charge with identifier " + chargeDefinition.getId() |
| + " cannot be applied: No valid loan disbursement available"; |
| throw new ChargeCannotBeAppliedToException("loan", errorMessage, chargeDefinition.getId()); |
| } |
| loan.addTrancheLoanCharge(chargeDefinition); |
| } else { |
| loanCharge = loanChargeAssembler.createNewFromJson(loan, chargeDefinition, command); |
| businessEventNotifierService.notifyPreBusinessEvent(new LoanAddChargeBusinessEvent(loanCharge)); |
| |
| validateAddLoanCharge(loan, chargeDefinition, loanCharge); |
| isAppliedOnBackDate = addCharge(loan, chargeDefinition, loanCharge); |
| if (loanCharge.getDueLocalDate() == null || DateUtils.isAfter(recalculateFrom, loanCharge.getDueLocalDate())) { |
| isAppliedOnBackDate = true; |
| recalculateFrom = loanCharge.getDueLocalDate(); |
| } |
| } |
| |
| boolean reprocessRequired = true; |
| if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) { |
| if (isAppliedOnBackDate && loan.isFeeCompoundingEnabledForInterestRecalculation()) { |
| |
| loan = runScheduleRecalculation(loan, recalculateFrom); |
| reprocessRequired = false; |
| } |
| this.loanWritePlatformService.updateOriginalSchedule(loan); |
| } |
| // [For Adv payment allocation strategy] check if charge due date is earlier than last transaction |
| // date, if yes trigger reprocess else no reprocessing |
| if (AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY.equals(loan.transactionProcessingStrategy())) { |
| LoanTransaction lastPaymentTransaction = loan.getLastPaymentTransaction(); |
| if (lastPaymentTransaction != null) { |
| if (loanCharge.getEffectiveDueDate() != null |
| && DateUtils.isAfter(loanCharge.getEffectiveDueDate(), lastPaymentTransaction.getTransactionDate())) { |
| reprocessRequired = false; |
| } |
| } |
| } |
| |
| if (reprocessRequired) { |
| ChangedTransactionDetail changedTransactionDetail = loan.reprocessTransactions(); |
| if (changedTransactionDetail != null) { |
| for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { |
| loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue()); |
| accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue()); |
| } |
| // Trigger transaction replayed event |
| replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail); |
| } |
| loan = loanAccountDomainService.saveAndFlushLoanWithDataIntegrityViolationChecks(loan); |
| |
| } |
| |
| postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); |
| |
| if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled() && isAppliedOnBackDate |
| && loan.isFeeCompoundingEnabledForInterestRecalculation()) { |
| this.loanAccountDomainService.recalculateAccruals(loan); |
| } |
| this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); |
| |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanAddChargeBusinessEvent(loanCharge)); |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan)); |
| return new CommandProcessingResultBuilder().withCommandId(command.commandId()) // |
| .withEntityId(loanCharge.getId()) // |
| .withEntityExternalId(loanCharge.getExternalId()) // |
| .withOfficeId(loan.getOfficeId()) // |
| .withClientId(loan.getClientId()) // |
| .withGroupId(loan.getGroupId()) // |
| .withLoanId(loanId) // |
| .build(); |
| } |
| |
| @Transactional |
| @Override |
| public CommandProcessingResult loanChargeRefund(final Long loanId, final JsonCommand command) { |
| |
| this.loanChargeApiJsonValidator.validateLoanChargeRefundTransaction(command.json()); |
| |
| final Long loanChargeId = command.longValueOfParameterNamed("loanChargeId"); |
| final LoanCharge loanCharge = retrieveLoanChargeBy(loanId, loanChargeId); |
| |
| final Integer installmentNumber = command.integerValueOfParameterNamed("installmentNumber"); |
| final LocalDate dueDate = command.localDateValueOfParameterNamed("dueDate"); |
| final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount"); |
| |
| final LoanInstallmentCharge installmentChargeEntry = loanChargeRefundEntranceValidation(loanCharge, installmentNumber, dueDate); |
| Integer installmentNumberIdentified = null; |
| if (installmentChargeEntry != null) { |
| installmentNumberIdentified = installmentChargeEntry.getRepaymentInstallment().getInstallmentNumber(); |
| } |
| final BigDecimal fullRefundAbleAmount = loanChargeValidateRefundAmount(loanCharge, installmentChargeEntry, transactionAmount); |
| |
| JsonCommand repaymentJsonCommand = adaptLoanChargeRefundCommandForFurtherRepaymentProcessing(command, fullRefundAbleAmount); |
| |
| boolean isRecoveryRepayment = false; |
| String chargeRefundChargeType = "F"; |
| if (loanCharge.isPenaltyCharge()) { |
| chargeRefundChargeType = "P"; |
| } |
| // chargeRefundChargeType only included as a parameter for accounting reason - in order to identify whether fee |
| // or penalty GL account is relevant |
| CommandProcessingResult result = loanWritePlatformService.makeLoanRepaymentWithChargeRefundChargeType( |
| LoanTransactionType.CHARGE_REFUND, repaymentJsonCommand.getLoanId(), repaymentJsonCommand, isRecoveryRepayment, |
| chargeRefundChargeType); |
| |
| Long loanChargeRefundTransactionId = result.getResourceId(); |
| LoanTransaction newChargeRefundTxn = null; |
| for (LoanTransaction chargeRefundTxn : loanCharge.getLoan().getLoanTransactions()) { |
| if (loanChargeRefundTransactionId.equals(chargeRefundTxn.getId())) { |
| newChargeRefundTxn = chargeRefundTxn; |
| final BigDecimal appliedRefundAmount = newChargeRefundTxn.getAmount(loanCharge.getLoan().getCurrency()).getAmount() |
| .multiply(BigDecimal.valueOf(-1)); |
| final LoanChargePaidBy loanChargePaidByForChargeRefund = new LoanChargePaidBy(newChargeRefundTxn, loanCharge, |
| appliedRefundAmount, installmentNumberIdentified); |
| newChargeRefundTxn.getLoanChargesPaid().add(loanChargePaidByForChargeRefund); |
| loanCharge.getLoanChargePaidBySet().add(loanChargePaidByForChargeRefund); |
| break; |
| } |
| } |
| if (newChargeRefundTxn != null) { |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(newChargeRefundTxn.getLoan())); |
| } |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanChargeRefundBusinessEvent(newChargeRefundTxn)); |
| return result; |
| } |
| |
| @Transactional |
| @Override |
| public CommandProcessingResult undoWaiveLoanCharge(final JsonCommand command) { |
| |
| LoanTransaction loanTransaction = this.loanTransactionRepository.findByIdAndLoanId(command.entityId(), command.getLoanId()) |
| .orElseThrow(() -> new LoanTransactionNotFoundException(command.entityId(), command.getLoanId())); |
| if (!loanTransaction.getTypeOf().getCode().equals(LoanTransactionType.WAIVE_CHARGES.getCode())) { |
| throw new InvalidLoanTransactionTypeException("transaction", "undo.waive.charge", "Transaction is not a waive charge type."); |
| } |
| if (!loanTransaction.isNotReversed()) { |
| throw new LoanChargeWaiveCannotBeReversedException( |
| LoanChargeWaiveCannotBeReversedException.LoanChargeWaiveCannotUndoReason.ALREADY_REVERSED, loanTransaction.getId()); |
| } |
| |
| Set<LoanChargePaidBy> loanChargePaidBySet = loanTransaction.getLoanChargesPaid(); |
| LoanChargePaidBy loanChargePaidBy = loanChargePaidBySet.stream().findFirst().orElseThrow(LoanChargeNotFoundException::new); |
| final LoanCharge loanCharge = loanChargePaidBy.getLoanCharge(); |
| // Validate loan charge is not already paid |
| if (loanCharge.isPaid()) { |
| throw new LoanChargeWaiveCannotBeReversedException( |
| LoanChargeWaiveCannotBeReversedException.LoanChargeWaiveCannotUndoReason.ALREADY_PAID, loanCharge.getId()); |
| } |
| |
| final Long loanId = loanTransaction.getLoan().getId(); |
| final Loan loan = this.loanAssembler.assembleFrom(loanId); |
| checkClientOrGroupActive(loan); |
| // Charges may be waived only when the loan associated with them are |
| // active |
| if (!loan.getStatus().isActive()) { |
| throw new LoanChargeWaiveCannotBeReversedException( |
| LoanChargeWaiveCannotBeReversedException.LoanChargeWaiveCannotUndoReason.LOAN_INACTIVE, loanCharge.getId()); |
| } |
| if (loan.isChargedOff() && !DateUtils.isAfter(loanTransaction.getTransactionDate(), loan.getChargedOffOnDate())) { |
| throw new GeneralPlatformDomainRuleException("error.msg.transaction.date.cannot.be.earlier.than.charge.off.date", |
| "Undo Loan transaction: " + loanTransaction.getId() |
| + " is not allowed before or on the date when the loan got charged-off", |
| loanTransaction.getId()); |
| } |
| final Map<String, Object> changes = new LinkedHashMap<>(); |
| |
| businessEventNotifierService.notifyPreBusinessEvent(new LoanWaiveChargeUndoBusinessEvent(loanCharge)); |
| |
| undoWaivedCharge(changes, loan, loanTransaction, loanChargePaidBy); |
| |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanWaiveChargeUndoBusinessEvent(loanCharge)); |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan)); |
| changes.put("principalPortion", loanTransaction.getPrincipalPortion()); |
| changes.put("interestPortion", loanTransaction.getInterestPortion()); |
| changes.put("feeChargesPortion", loanTransaction.getFeeChargesPortion()); |
| changes.put("penaltyChargesPortion", loanTransaction.getPenaltyChargesPortion()); |
| changes.put("outstandingLoanBalance", loanTransaction.getOutstandingLoanBalance()); |
| changes.put("id", loanTransaction.getId()); |
| changes.put("externalId", loanTransaction.getExternalId()); |
| changes.put("date", loanTransaction.getTransactionDate()); |
| |
| return new CommandProcessingResultBuilder() // |
| .withCommandId(command.commandId()) // |
| .withEntityId(loanCharge.getId()) // |
| .withEntityExternalId(loanCharge.getExternalId()) // |
| .withSubEntityId(loanTransaction.getId()) // |
| .withSubEntityExternalId(loanTransaction.getExternalId()) // |
| .withLoanId(loanId) // |
| .with(changes).build(); |
| } |
| |
| @Transactional |
| @Override |
| public CommandProcessingResult updateLoanCharge(final Long loanId, final Long loanChargeId, final JsonCommand command) { |
| |
| this.loanChargeApiJsonValidator.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.getStatus().isSubmittedAndPendingApproval()) { |
| throw new LoanChargeCannotBeUpdatedException( |
| LoanChargeCannotBeUpdatedException.LoanChargeCannotBeUpdatedReason.LOAN_NOT_IN_SUBMITTED_AND_PENDING_APPROVAL_STAGE, |
| loanCharge.getId()); |
| } |
| businessEventNotifierService.notifyPreBusinessEvent(new LoanUpdateChargeBusinessEvent(loanCharge)); |
| |
| final Map<String, Object> changes = loan.updateLoanCharge(loanCharge, command); |
| |
| this.loanRepositoryWrapper.save(loan); |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanUpdateChargeBusinessEvent(loanCharge)); |
| return new CommandProcessingResultBuilder() // |
| .withCommandId(command.commandId()) // |
| .withEntityId(loanChargeId) // |
| .withEntityExternalId(loanCharge.getExternalId()) // |
| .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) { |
| |
| final Loan loan = this.loanAssembler.assembleFrom(loanId); |
| checkClientOrGroupActive(loan); |
| this.loanChargeApiJsonValidator.validateInstallmentChargeTransaction(command.json()); |
| final ExternalId externalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName); |
| final LoanCharge loanCharge = retrieveLoanChargeBy(loanId, loanChargeId); |
| |
| // Charges may be waived only when the loan associated with them are |
| // active |
| if (!loan.getStatus().isActive()) { |
| throw new LoanChargeCannotBeWaivedException(LoanChargeCannotBeWaivedException.LoanChargeCannotBeWaivedReason.LOAN_INACTIVE, |
| loanCharge.getId()); |
| } |
| |
| // validate loan charge is not already paid or waived |
| if (loanCharge.isWaived()) { |
| throw new LoanChargeCannotBeWaivedException(LoanChargeCannotBeWaivedException.LoanChargeCannotBeWaivedReason.ALREADY_WAIVED, |
| loanCharge.getId()); |
| } else if (loanCharge.isPaid()) { |
| throw new LoanChargeCannotBeWaivedException(LoanChargeCannotBeWaivedException.LoanChargeCannotBeWaivedReason.ALREADY_PAID, |
| loanCharge.getId()); |
| } |
| businessEventNotifierService.notifyPreBusinessEvent(new LoanWaiveChargeBusinessEvent(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(LoanChargeCannotBePayedException.LoanChargeCannotBePayedReason.ALREADY_WAIVED, |
| loanCharge.getId()); |
| } else if (chargePerInstallment.isPaid()) { |
| throw new LoanChargeCannotBePayedException(LoanChargeCannotBePayedException.LoanChargeCannotBePayedReason.ALREADY_PAID, |
| loanCharge.getId()); |
| } |
| loanInstallmentNumber = chargePerInstallment.getRepaymentInstallment().getInstallmentNumber(); |
| } |
| |
| final Map<String, Object> changes = new LinkedHashMap<>(); |
| changes.put(LoanApiConstants.externalIdParameterName, externalId); |
| |
| 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> chargePaidByCollection = this.loanChargeReadPlatformService |
| .retrieveLoanChargesPaidBy(loanCharge.getId(), LoanTransactionType.ACCRUAL, loanInstallmentNumber); |
| for (LoanChargePaidByData chargePaidByData : chargePaidByCollection) { |
| accruedCharge = accruedCharge.plus(chargePaidByData.getAmount()); |
| } |
| } |
| |
| final LoanTransaction waiveTransaction = loan.waiveLoanCharge(loanCharge, defaultLoanLifecycleStateMachine, changes, |
| existingTransactionIds, existingReversedTransactionIds, loanInstallmentNumber, scheduleGeneratorDTO, accruedCharge, |
| externalId); |
| |
| this.loanTransactionRepository.saveAndFlush(waiveTransaction); |
| this.loanRepositoryWrapper.save(loan); |
| |
| postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); |
| this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); |
| loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanWaiveChargeBusinessEvent(loanCharge)); |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan)); |
| return new CommandProcessingResultBuilder() // |
| .withCommandId(command.commandId()) // |
| .withEntityId(loanChargeId) // |
| .withEntityExternalId(loanCharge.getExternalId()) // |
| .withSubEntityId(waiveTransaction.getId()) // |
| .withSubEntityExternalId(waiveTransaction.getExternalId()) // |
| .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.getStatus().isSubmittedAndPendingApproval()) { |
| throw new LoanChargeCannotBeDeletedException( |
| LoanChargeCannotBeDeletedException.LoanChargeCannotBeDeletedReason.LOAN_NOT_IN_SUBMITTED_AND_PENDING_APPROVAL_STAGE, |
| loanCharge.getId()); |
| } |
| businessEventNotifierService.notifyPreBusinessEvent(new LoanDeleteChargeBusinessEvent(loanCharge)); |
| |
| loan.removeLoanCharge(loanCharge); |
| this.loanRepositoryWrapper.save(loan); |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanDeleteChargeBusinessEvent(loanCharge)); |
| return new CommandProcessingResultBuilder() // |
| .withCommandId(command.commandId()) // |
| .withEntityId(loanChargeId) // |
| .withEntityExternalId(loanCharge.getExternalId()) // |
| .withOfficeId(loan.getOfficeId()) // |
| .withClientId(loan.getClientId()) // |
| .withGroupId(loan.getGroupId()) // |
| .withLoanId(loanId) // |
| .build(); |
| } |
| |
| @Transactional |
| @Override |
| public CommandProcessingResult payLoanCharge(final Long loanId, Long loanChargeId, final JsonCommand command, |
| final boolean isChargeIdIncludedInJson) { |
| |
| this.loanChargeApiJsonValidator.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); |
| final ExternalId externalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName); |
| |
| // Charges may be waived only when the loan associated with them are |
| // active |
| if (!loan.getStatus().isActive()) { |
| throw new LoanChargeCannotBePayedException(LoanChargeCannotBePayedException.LoanChargeCannotBePayedReason.LOAN_INACTIVE, |
| loanCharge.getId()); |
| } |
| |
| // validate loan charge is not already paid or waived |
| if (loanCharge.isWaived()) { |
| throw new LoanChargeCannotBePayedException(LoanChargeCannotBePayedException.LoanChargeCannotBePayedReason.ALREADY_WAIVED, |
| loanCharge.getId()); |
| } else if (loanCharge.isPaid()) { |
| throw new LoanChargeCannotBePayedException(LoanChargeCannotBePayedException.LoanChargeCannotBePayedReason.ALREADY_PAID, |
| loanCharge.getId()); |
| } |
| |
| if (!loanCharge.getChargePaymentMode().isPaymentModeAccountTransfer()) { |
| throw new LoanChargeCannotBePayedException( |
| LoanChargeCannotBePayedException.LoanChargeCannotBePayedReason.CHARGE_NOT_ACCOUNT_TRANSFER, loanCharge.getId()); |
| } |
| |
| final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); |
| |
| final Locale locale = command.extractLocale(); |
| final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(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(LoanChargeCannotBePayedException.LoanChargeCannotBePayedReason.ALREADY_WAIVED, |
| loanCharge.getId()); |
| } else if (chargePerInstallment.isPaid()) { |
| throw new LoanChargeCannotBePayedException(LoanChargeCannotBePayedException.LoanChargeCannotBePayedReason.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.getId(), loanId, "Loan Charge Payment", locale, fmt, null, null, |
| LoanTransactionType.CHARGE_PAYMENT.getValue(), loanChargeId, loanInstallmentNumber, |
| AccountTransferType.CHARGE_PAYMENT.getValue(), null, null, externalId, null, null, fromSavingsAccount, isRegularTransaction, |
| isExceptionForBalanceCheck); |
| Long transferTransactionId = this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO); |
| AccountTransferDetails transferDetails = this.accountTransferDetailRepository.findById(transferTransactionId) |
| .orElseThrow(() -> new AccountTransferNotFoundException(transferTransactionId)); |
| LoanTransaction loanTransaction = transferDetails.getAccountTransferTransactions().get(0).getToLoanTransaction(); |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan)); |
| return new CommandProcessingResultBuilder() // |
| .withCommandId(command.commandId()) // |
| .withEntityId(loanChargeId) // |
| .withEntityExternalId(loanCharge.getExternalId()) // |
| .withSubEntityId(loanTransaction.getId()) // |
| .withSubEntityExternalId(loanTransaction.getExternalId()) // |
| .withOfficeId(loan.getOfficeId()) // |
| .withClientId(loan.getClientId()) // |
| .withGroupId(loan.getGroupId()) // |
| .withLoanId(loanId) // |
| .withSavingsId(portfolioAccountData.getId()).build(); |
| } |
| |
| @Transactional |
| @Override |
| public CommandProcessingResult adjustmentForLoanCharge(Long loanId, Long loanChargeId, JsonCommand command) { |
| this.loanChargeApiJsonValidator.validateLoanChargeAdjustmentRequest(loanId, loanChargeId, command.json()); |
| |
| final LoanCharge loanCharge = retrieveLoanChargeBy(loanId, loanChargeId); |
| final LocalDate transactionDate = DateUtils.getBusinessLocalDate(); |
| final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("amount"); |
| final ExternalId externalId = externalIdFactory.createFromCommand(command, "externalId"); |
| final String locale = command.locale(); |
| |
| Map<String, Object> changes = new HashMap<>(); |
| changes.put("externalId", externalId); |
| changes.put("amount", transactionAmount); |
| changes.put("transactionDate", transactionDate); |
| changes.put("locale", locale); |
| |
| loanChargeAdjustmentEntranceValidation(loanCharge, transactionAmount); |
| final Loan loan = loanAssembler.assembleFrom(loanId); |
| |
| final CommandProcessingResultBuilder commandProcessingResultBuilder = new CommandProcessingResultBuilder(); |
| PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createPaymentDetail(command, changes); |
| if (paymentDetail != null) { |
| paymentDetail = this.paymentDetailWritePlatformService.persistPaymentDetail(paymentDetail); |
| } |
| LoanTransaction loanTransaction = applyChargeAdjustment(loan, loanCharge, transactionAmount, transactionDate, externalId, |
| paymentDetail); |
| |
| // Update loan transaction on repayment. |
| if (AccountType.fromInt(loan.getLoanType()).isIndividualAccount()) { |
| Set<LoanCollateralManagement> loanCollateralManagements = loan.getLoanCollateralManagements(); |
| for (LoanCollateralManagement loanCollateralManagement : loanCollateralManagements) { |
| loanCollateralManagement.setLoanTransactionData(loanTransaction); |
| ClientCollateralManagement clientCollateralManagement = loanCollateralManagement.getClientCollateralManagement(); |
| |
| if (loan.getStatus().isClosed()) { |
| loanCollateralManagement.setIsReleased(true); |
| BigDecimal quantity = loanCollateralManagement.getQuantity(); |
| clientCollateralManagement.updateQuantity(clientCollateralManagement.getQuantity().add(quantity)); |
| loanCollateralManagement.setClientCollateralManagement(clientCollateralManagement); |
| } |
| } |
| this.loanAccountDomainService.updateLoanCollateralTransaction(loanCollateralManagements); |
| } |
| |
| final String noteText = command.stringValueOfParameterNamed("note"); |
| if (StringUtils.isNotBlank(noteText)) { |
| final Note note = Note.loanNote(loan, noteText); |
| changes.put("note", noteText); |
| this.noteRepository.save(note); |
| } |
| |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan)); |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanChargeAdjustmentPostBusinessEvent(loanTransaction)); |
| |
| return commandProcessingResultBuilder.withCommandId(command.commandId()) // |
| .withLoanId(loanId) // |
| .withEntityId(loanChargeId) // |
| .withEntityExternalId(loanCharge.getExternalId()) // |
| .withSubEntityId(loanTransaction.getId()) // |
| .withSubEntityExternalId(loanTransaction.getExternalId()) // |
| .with(changes) // |
| .build(); |
| } |
| |
| @Transactional |
| @Override |
| public void applyOverdueChargesForLoan(final Long loanId, Collection<OverdueLoanScheduleData> overdueLoanScheduleDataList) { |
| |
| Loan loan = this.loanAssembler.assembleFrom(loanId); |
| if (loan.isChargedOff()) { |
| log.warn("Adding charge to Loan: {} is not allowed. Loan Account is Charged-off", loanId); |
| return; |
| } |
| final List<Long> existingTransactionIds = loan.findExistingTransactionIds(); |
| final List<Long> existingReversedTransactionIds = loan.findExistingReversedTransactionIds(); |
| boolean runInterestRecalculation = false; |
| LocalDate recalculateFrom = DateUtils.getBusinessLocalDate(); |
| LocalDate lastChargeDate = null; |
| for (final OverdueLoanScheduleData overdueInstallment : overdueLoanScheduleDataList) { |
| |
| 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, null, null, null); |
| LoanOverdueDTO overdueDTO = applyChargeToOverdueLoanInstallment(loan, overdueInstallment.getChargeId(), |
| overdueInstallment.getPeriodNumber(), command); |
| loan = overdueDTO.getLoan(); |
| runInterestRecalculation = runInterestRecalculation || overdueDTO.isRunInterestRecalculation(); |
| if (DateUtils.isAfter(recalculateFrom, overdueDTO.getRecalculateFrom())) { |
| recalculateFrom = overdueDTO.getRecalculateFrom(); |
| } |
| if (lastChargeDate == null || DateUtils.isAfter(overdueDTO.getLastChargeAppliedDate(), lastChargeDate)) { |
| lastChargeDate = overdueDTO.getLastChargeAppliedDate(); |
| } |
| } |
| if (loan != null) { |
| boolean reprocessRequired = true; |
| LocalDate recalculatedTill = loan.fetchInterestRecalculateFromDate(); |
| if (DateUtils.isAfter(recalculateFrom, recalculatedTill)) { |
| recalculateFrom = recalculatedTill; |
| } |
| |
| if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) { |
| if (runInterestRecalculation && loan.isFeeCompoundingEnabledForInterestRecalculation()) { |
| loan = runScheduleRecalculation(loan, recalculateFrom); |
| reprocessRequired = false; |
| } |
| this.loanWritePlatformService.updateOriginalSchedule(loan); |
| } |
| |
| if (reprocessRequired) { |
| addInstallmentIfPenaltyAppliedAfterLastDueDate(loan, lastChargeDate); |
| ChangedTransactionDetail changedTransactionDetail = loan.reprocessTransactions(); |
| if (changedTransactionDetail != null) { |
| for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings() |
| .entrySet()) { |
| loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue()); |
| accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue()); |
| } |
| // Trigger transaction replayed event |
| replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail); |
| } |
| loan = loanAccountDomainService.saveAndFlushLoanWithDataIntegrityViolationChecks(loan); |
| } |
| |
| postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); |
| |
| if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled() && runInterestRecalculation |
| && loan.isFeeCompoundingEnabledForInterestRecalculation()) { |
| this.loanAccountDomainService.recalculateAccruals(loan); |
| } |
| this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); |
| } |
| } |
| |
| private LoanTransaction applyChargeAdjustment(final Loan loan, final LoanCharge loanCharge, final BigDecimal transactionAmount, |
| final LocalDate transactionDate, final ExternalId txnExternalId, PaymentDetail paymentDetail) { |
| businessEventNotifierService.notifyPreBusinessEvent(new LoanChargeAdjustmentPreBusinessEvent(loan)); |
| final List<Long> existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds()); |
| final List<Long> existingReversedTransactionIds = new ArrayList<>(loan.findExistingReversedTransactionIds()); |
| |
| LoanTransaction loanChargeAdjustmentTransaction = LoanTransaction.chargeAdjustment(loan, transactionAmount, transactionDate, |
| txnExternalId, paymentDetail); |
| LoanTransactionRelation loanTransactionRelation = LoanTransactionRelation.linkToCharge(loanChargeAdjustmentTransaction, loanCharge, |
| LoanTransactionRelationTypeEnum.CHARGE_ADJUSTMENT); |
| loanChargeAdjustmentTransaction.getLoanTransactionRelations().add(loanTransactionRelation); |
| |
| loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(loanChargeAdjustmentTransaction); |
| |
| defaultLoanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, loan); |
| final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loanRepaymentScheduleTransactionProcessorFactory |
| .determineProcessor(loan.transactionProcessingStrategy()); |
| loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanChargeAdjustmentTransaction, loan.getCurrency(), |
| loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), loan.getTotalOverpaidAsMoney()); |
| |
| loan.addLoanTransaction(loanChargeAdjustmentTransaction); |
| loan.updateLoanSummaryAndStatus(); |
| |
| loanAccountDomainService.saveAndFlushLoanWithDataIntegrityViolationChecks(loan); |
| |
| final Map<String, Object> accountingBridgeData = loan.deriveAccountingBridgeData(loan.getCurrency().getCode(), |
| existingTransactionIds, existingReversedTransactionIds, false); |
| this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData); |
| loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); |
| loanAccountDomainService.setLoanDelinquencyTag(loan, transactionDate); |
| |
| return loanChargeAdjustmentTransaction; |
| } |
| |
| private void undoWaivedCharge(final Map<String, Object> changes, final Loan loan, final LoanTransaction loanTransaction, |
| final LoanChargePaidBy loanChargePaidBy) { |
| switch (loanChargePaidBy.getLoanCharge().getChargeTimeType()) { |
| case SPECIFIED_DUE_DATE -> undoSpecifiedDueDateCharge(changes, loan, loanTransaction, loanChargePaidBy); |
| case INSTALMENT_FEE -> undoInstalmentFee(changes, loan, loanTransaction, loanChargePaidBy); |
| default -> throw new UnsupportedOperationException( |
| "Undo waive charge is not support for this charge: " + loanChargePaidBy.getLoanCharge().getChargeTimeType()); |
| } |
| } |
| |
| private void undoInstalmentFee(Map<String, Object> changes, Loan loan, LoanTransaction loanTransaction, |
| LoanChargePaidBy loanChargePaidBy) { |
| final List<Long> existingTransactionIds = loan.findExistingTransactionIds(); |
| final List<Long> existingReversedTransactionIds = loan.findExistingReversedTransactionIds(); |
| LoanCharge loanCharge = loanChargePaidBy.getLoanCharge(); |
| final Integer installmentNumber = loanChargePaidBy.getInstallmentNumber(); |
| LoanInstallmentCharge chargePerInstallment; |
| // final Integer installmentNumber = command.integerValueOfParameterNamed("installmentNumber"); |
| if (installmentNumber != null) { |
| // Get installment charge. |
| chargePerInstallment = loanCharge.getInstallmentLoanCharge(installmentNumber); |
| // Get installment amount waived. |
| BigDecimal amountWaived = chargePerInstallment.getAmountWaived(loan.getCurrency()).getAmount(); |
| // Check whether the installment charge is not waived. If so throw new error |
| if (!chargePerInstallment.isWaived() || amountWaived == null) { |
| throw new LoanChargeWaiveCannotBeReversedException( |
| LoanChargeWaiveCannotBeReversedException.LoanChargeWaiveCannotUndoReason.NOT_WAIVED, loanCharge.getId()); |
| } |
| // Reverse waived transaction |
| loanTransaction.reverse(); |
| // Set manually adjusted value to `1` |
| loanTransaction.setManuallyAdjustedOrReversed(); |
| // Get loan charge outstanding amount |
| BigDecimal amountOutstanding = loanCharge.getAmountOutstanding(loan.getCurrency()).getAmount(); |
| // Add the amount waived to outstanding amount |
| loanCharge.setOutstandingAmount(amountOutstanding.add(amountWaived)); |
| // Get loan charge total amount waived |
| BigDecimal totalAmountWaved = loanCharge.getAmountWaived(loan.getCurrency()).getAmount(); |
| // Subtract the amount waived from the existing amount waived. |
| loanCharge.setAmountWaived(totalAmountWaved.subtract(amountWaived)); |
| // Get installment outstanding amount |
| BigDecimal amountOutstandingPerInstallment = chargePerInstallment.getAmountOutstanding(); |
| // Add the amount waived to the outstanding amount of the installment |
| chargePerInstallment.setOutstandingAmount(amountOutstandingPerInstallment.add(amountWaived)); |
| // Set the amount waived value to ZERO |
| chargePerInstallment.setAmountWaived(null); |
| // Reset waived flag |
| chargePerInstallment.undoWaiveFlag(); |
| // Update installment balances |
| updateRepaymentInstalmentWithWaivedAmount(loanCharge, chargePerInstallment.getInstallment(), amountWaived); |
| // Update loan charge. |
| loanCharge.setInstallmentLoanCharge(chargePerInstallment, chargePerInstallment.getInstallment().getInstallmentNumber()); |
| if (loanCharge.amount().compareTo(loanCharge.amountOutstanding()) == 0 && loanCharge.isWaived()) { |
| loanCharge.undoWaived(); |
| } |
| loan.updateLoanSummaryForUndoWaiveCharge(amountWaived, loanCharge.isPenaltyCharge()); |
| postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); |
| changes.put(AMOUNT, amountWaived); |
| } else { |
| throw new InstallmentNotFoundException(loanTransaction.getId()); |
| } |
| } |
| |
| private void undoSpecifiedDueDateCharge(final Map<String, Object> changes, final Loan loan, final LoanTransaction loanTransaction, |
| final LoanChargePaidBy loanChargePaidBy) { |
| |
| final List<Long> existingTransactionIds = loan.findExistingTransactionIds(); |
| final List<Long> existingReversedTransactionIds = loan.findExistingReversedTransactionIds(); |
| LoanCharge loanCharge = loanChargePaidBy.getLoanCharge(); |
| BigDecimal amountWaived = loanCharge.getAmountWaived(loan.getCurrency()).getAmount(); |
| if (!loanCharge.isWaived() || amountWaived == null) { |
| throw new LoanChargeWaiveCannotBeReversedException( |
| LoanChargeWaiveCannotBeReversedException.LoanChargeWaiveCannotUndoReason.NOT_WAIVED, loanCharge.getId()); |
| } |
| loanTransaction.reverse(); |
| loanTransaction.setManuallyAdjustedOrReversed(); |
| loanCharge.setOutstandingAmount(loanCharge.amountOutstanding().add(amountWaived)); |
| loanCharge.setAmountWaived(null); |
| loanCharge.undoWaived(); |
| LoanRepaymentScheduleInstallment installment = loan.getRepaymentScheduleInstallments().stream() |
| .filter(e -> isPartOfThisInstallment(loanCharge, e)).findFirst().orElseThrow(); |
| updateRepaymentInstalmentWithWaivedAmount(loanCharge, installment, amountWaived); |
| loan.updateLoanSummaryForUndoWaiveCharge(amountWaived, loanCharge.isPenaltyCharge()); |
| postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); |
| changes.put(AMOUNT, amountWaived); |
| } |
| |
| private void updateRepaymentInstalmentWithWaivedAmount(final LoanCharge loanCharge, final LoanRepaymentScheduleInstallment installment, |
| final BigDecimal amountWaived) { |
| if (loanCharge.isPenaltyCharge()) { |
| // Get the penalty charges waived amount per installment |
| BigDecimal penaltyChargesWaivedAmount = installment.getPenaltyChargesWaived(loanCharge.getLoan().getCurrency()).getAmount(); |
| // Subtract the amount waived from the existing fee charges waived amount. |
| installment.setPenaltyChargesWaived(penaltyChargesWaivedAmount.subtract(amountWaived)); |
| } else { |
| // Get the fee charges waived amount per installment |
| BigDecimal feeChargesWaivedAmount = installment.getFeeChargesWaived(loanCharge.getLoan().getCurrency()).getAmount(); |
| // Subtract the amount waived from the existing fee charges waived amount. |
| installment.setFeeChargesWaived(feeChargesWaivedAmount.subtract(amountWaived)); |
| } |
| } |
| |
| private void validateAddingNewChargeAllowed(List<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"); |
| } |
| } |
| |
| 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 |
| && DateUtils.isBefore(loanCharge.getDueLocalDate(), 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.getStatus().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.loanChargeApiJsonValidator.validateLoanCharges(loanCharges, dataValidationErrors); |
| if (!dataValidationErrors.isEmpty()) { |
| throw new PlatformApiDataValidationException(dataValidationErrors); |
| } |
| } |
| } |
| |
| private boolean addCharge(final Loan loan, final Charge chargeDefinition, LoanCharge loanCharge) { |
| |
| 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()); |
| } |
| } |
| |
| if (!loan.isInterestBearing() && loanCharge.isSpecifiedDueDate()) { |
| LoanRepaymentScheduleInstallment latestRepaymentScheduleInstalment = loan.getRepaymentScheduleInstallments() |
| .get(loan.getLoanRepaymentScheduleInstallmentsSize() - 1); |
| if (DateUtils.isAfter(loanCharge.getDueDate(), latestRepaymentScheduleInstalment.getDueDate())) { |
| if (latestRepaymentScheduleInstalment.isAdditional()) { |
| latestRepaymentScheduleInstalment.updateDueDate(loanCharge.getDueDate()); |
| } else { |
| final LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loan, |
| (loan.getLoanRepaymentScheduleInstallmentsSize() + 1), latestRepaymentScheduleInstalment.getDueDate(), |
| loanCharge.getDueDate(), BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, false, null); |
| installment.markAsAdditional(); |
| loan.addLoanRepaymentScheduleInstallment(installment); |
| } |
| } |
| } |
| |
| loan.addLoanCharge(loanCharge); |
| |
| loanCharge = this.loanChargeRepository.saveAndFlush(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.getStatus().isActive() && loan.isNoneOrCashOrUpfrontAccrualAccountingEnabledOnLoanProduct()) { |
| final LoanTransaction applyLoanChargeTransaction = loan.handleChargeAppliedTransaction(loanCharge, null); |
| this.loanTransactionRepository.saveAndFlush(applyLoanChargeTransaction); |
| } |
| return DateUtils.isBeforeBusinessDate(loanCharge.getDueLocalDate()); |
| } |
| |
| private LoanOverdueDTO applyChargeToOverdueLoanInstallment(final Loan loan, final Long loanChargeId, final Integer periodNumber, |
| final JsonCommand command) { |
| boolean runInterestRecalculation = false; |
| final Charge chargeDefinition = this.chargeRepository.findOneWithNotFoundDetection(loanChargeId); |
| |
| Collection<Integer> frequencyNumbers = loanChargeReadPlatformService.retrieveOverdueInstallmentChargeFrequencyNumber(loan, |
| chargeDefinition, 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 < 1) { |
| diff = 1L; |
| } |
| LocalDate startDate = dueDate.plusDays(penaltyWaitPeriodValue + 1L); |
| int frequencyNumber = 1; |
| if (feeFrequency == null) { |
| scheduleDates.put(frequencyNumber++, startDate.minusDays(diff)); |
| } else { |
| while (!DateUtils.isDateInTheFuture(startDate)) { |
| scheduleDates.put(frequencyNumber++, startDate.minusDays(diff)); |
| |
| startDate = scheduledDateGenerator.getRepaymentPeriodDate(PeriodFrequencyType.fromInt(feeFrequency), |
| chargeDefinition.feeInterval(), startDate); |
| } |
| } |
| |
| for (Integer frequency : frequencyNumbers) { |
| scheduleDates.remove(frequency); |
| } |
| |
| LoanRepaymentScheduleInstallment installment = null; |
| LocalDate lastChargeAppliedDate = dueDate; |
| LocalDate recalculateFrom = DateUtils.getBusinessLocalDate(); |
| if (!scheduleDates.isEmpty()) { |
| installment = loan.fetchRepaymentScheduleInstallment(periodNumber); |
| lastChargeAppliedDate = installment.getDueDate(); |
| businessEventNotifierService.notifyPreBusinessEvent(new LoanApplyOverdueChargeBusinessEvent(loan)); |
| |
| for (Map.Entry<Integer, LocalDate> entry : scheduleDates.entrySet()) { |
| |
| final LoanCharge loanCharge = loanChargeAssembler.createNewFromJson(loan, chargeDefinition, command, entry.getValue()); |
| |
| if (BigDecimal.ZERO.compareTo(loanCharge.amount()) == 0) { |
| continue; |
| } |
| LoanOverdueInstallmentCharge overdueInstallmentCharge = new LoanOverdueInstallmentCharge(loanCharge, installment, |
| entry.getKey()); |
| loanCharge.updateOverdueInstallmentCharge(overdueInstallmentCharge); |
| |
| boolean isAppliedOnBackDate = addCharge(loan, chargeDefinition, loanCharge); |
| runInterestRecalculation = runInterestRecalculation || isAppliedOnBackDate; |
| if (DateUtils.isBefore(entry.getValue(), recalculateFrom)) { |
| recalculateFrom = entry.getValue(); |
| } |
| if (DateUtils.isAfter(entry.getValue(), lastChargeAppliedDate)) { |
| lastChargeAppliedDate = entry.getValue(); |
| } |
| } |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanApplyOverdueChargeBusinessEvent(loan)); |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan)); |
| } |
| |
| return new LoanOverdueDTO(loan, runInterestRecalculation, recalculateFrom, lastChargeAppliedDate); |
| } |
| |
| private void addInstallmentIfPenaltyAppliedAfterLastDueDate(Loan loan, LocalDate lastChargeDate) { |
| if (lastChargeDate != null) { |
| List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments(); |
| LoanRepaymentScheduleInstallment lastInstallment = loan.fetchRepaymentScheduleInstallment(installments.size()); |
| if (DateUtils.isAfter(lastChargeDate, 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; |
| final Set<LoanInterestRecalcualtionAdditionalDetails> compoundingDetails = null; |
| LoanRepaymentScheduleInstallment newEntry = new LoanRepaymentScheduleInstallment(loan, installments.size() + 1, |
| lastInstallment.getDueDate(), lastChargeDate, principal, interest, feeCharges, penaltyCharges, |
| recalculatedInterestComponent, compoundingDetails); |
| loan.addLoanRepaymentScheduleInstallment(newEntry); |
| } |
| } |
| } |
| |
| public Loan runScheduleRecalculation(Loan loan, final LocalDate recalculateFrom) { |
| if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) { |
| final List<Long> existingTransactionIds = loan.findExistingTransactionIds(); |
| ScheduleGeneratorDTO generatorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); |
| ChangedTransactionDetail changedTransactionDetail = loan |
| .handleRegenerateRepaymentScheduleWithInterestRecalculation(generatorDTO); |
| if (changedTransactionDetail != null) { |
| for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { |
| loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue()); |
| accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue()); |
| } |
| // Trigger transaction replayed event |
| replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail); |
| } |
| loan = loanAccountDomainService.saveAndFlushLoanWithDataIntegrityViolationChecks(loan); |
| loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); |
| } |
| return loan; |
| } |
| |
| private JsonCommand adaptLoanChargeRefundCommandForFurtherRepaymentProcessing(JsonCommand command, BigDecimal fullRefundAbleAmount) { |
| // creates JsonCommand for onward repayment processing |
| JsonObject jsonObject = (JsonObject) this.fromApiJsonHelper.parse(command.json()); |
| |
| String dateFormat; |
| if (this.fromApiJsonHelper.parameterExists("dateFormat", jsonObject)) { |
| dateFormat = this.fromApiJsonHelper.extractStringNamed("dateFormat", jsonObject); |
| } else { |
| dateFormat = "dd MMMM yyyy"; |
| jsonObject.addProperty("dateFormat", dateFormat); |
| } |
| DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(dateFormat); |
| LocalDate transactionDate = DateUtils.getBusinessLocalDate(); |
| String transactionDateString = transactionDate.format(dateTimeFormatter); |
| jsonObject.addProperty("transactionDate", transactionDateString); |
| if (!this.fromApiJsonHelper.parameterExists("transactionAmount", jsonObject)) { |
| jsonObject.addProperty("transactionAmount", fullRefundAbleAmount.toString()); |
| } |
| jsonObject.remove("loanChargeId"); |
| jsonObject.remove("installmentNumber"); |
| jsonObject.remove("dueDate"); |
| |
| return JsonCommand.fromExistingCommand(command, jsonObject); |
| } |
| |
| private BigDecimal loanChargeValidateRefundAmount(LoanCharge loanCharge, LoanInstallmentCharge installmentChargeEntry, |
| BigDecimal transactionAmount) { |
| // if transactionAmount not provided return max refundable amount (amount paid minus previous refunds) |
| BigDecimal chargeAmountPaid; |
| BigDecimal chargeAmountRefunded = BigDecimal.ZERO; |
| MonetaryCurrency loanCurrency = loanCharge.getLoan().getCurrency(); |
| if (loanCharge.isInstalmentFee() && installmentChargeEntry != null) { |
| final Integer installmentNumber = installmentChargeEntry.getRepaymentInstallment().getInstallmentNumber(); |
| chargeAmountPaid = installmentChargeEntry.getAmountPaid(loanCurrency).getAmount(); |
| for (LoanChargePaidBy loanChargePaidBy : loanCharge.getLoanChargePaidBySet()) { |
| if (installmentNumber.equals(loanChargePaidBy.getInstallmentNumber()) && isRefundElementOfChargeRefund(loanChargePaidBy)) { |
| chargeAmountRefunded = chargeAmountRefunded.add(loanChargePaidBy.getAmount()); |
| } |
| } |
| } else { |
| chargeAmountPaid = loanCharge.getAmountPaid(loanCurrency).getAmount(); |
| for (LoanChargePaidBy loanChargePaidBy : loanCharge.getLoanChargePaidBySet()) { |
| if (isRefundElementOfChargeRefund(loanChargePaidBy)) { |
| chargeAmountRefunded = chargeAmountRefunded.add(loanChargePaidBy.getAmount()); |
| } |
| } |
| } |
| chargeAmountRefunded = chargeAmountRefunded.multiply(BigDecimal.valueOf(-1)); |
| |
| if (chargeAmountRefunded.compareTo(chargeAmountPaid) > 0) { |
| final String errorMessage = "loan.charge.more.refunded.than.paid.unexpected.system.error"; |
| final String details = "Paid: " + chargeAmountPaid.toString() + " Refunded: " + chargeAmountPaid; |
| throw new LoanChargeRefundException(errorMessage, details); |
| } |
| |
| BigDecimal refundableAmount = chargeAmountPaid.subtract(chargeAmountRefunded); |
| // refund amount was provided. |
| if (transactionAmount != null && transactionAmount.compareTo(refundableAmount) > 0) { |
| final String errorMessage = "loan.charge.transaction.amount.is.more.than.is.refundable"; |
| final String details = "transactionAmount: " + transactionAmount + " Refundable: " + refundableAmount; |
| throw new LoanChargeRefundException(errorMessage, details); |
| } |
| |
| return refundableAmount; |
| } |
| |
| private void postJournalEntries(final Loan loan, final List<Long> existingTransactionIds, |
| final List<Long> existingReversedTransactionIds) { |
| |
| final MonetaryCurrency currency = loan.getCurrency(); |
| boolean isAccountTransfer = false; |
| final Map<String, Object> accountingBridgeData = loan.deriveAccountingBridgeData(currency.getCode(), existingTransactionIds, |
| existingReversedTransactionIds, isAccountTransfer); |
| this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData); |
| } |
| |
| private LoanCharge retrieveLoanChargeBy(final Long loanId, final Long loanChargeId) { |
| final LoanCharge loanCharge = this.loanChargeRepository.findById(loanChargeId) |
| .orElseThrow(() -> new LoanChargeNotFoundException(loanChargeId)); |
| |
| if (loanCharge.hasNotLoanIdentifiedBy(loanId)) { |
| throw new LoanChargeNotFoundException(loanChargeId, loanId); |
| } |
| return loanCharge; |
| } |
| |
| private boolean isRefundElementOfChargeRefund(LoanChargePaidBy loanChargePaidBy) { |
| // The Refund Element is always negative |
| return (loanChargePaidBy.getLoanTransaction().isChargeRefund() && loanChargePaidBy.getAmount().compareTo(BigDecimal.ZERO) < 0); |
| } |
| |
| private LoanInstallmentCharge loanChargeRefundEntranceValidation(LoanCharge loanCharge, Integer installmentNumber, LocalDate dueDate) { |
| |
| LoanInstallmentCharge installmentChargeEntry = null; |
| |
| Loan loan = loanCharge.getLoan(); |
| if (!(loan.isOpen() || loan.getStatus().isClosedObligationsMet() || loan.getStatus().isOverpaid())) { |
| final String errorMessage = "loan.charge.refund.invalid.status"; |
| throw new LoanChargeRefundException(errorMessage, loan.getStatus().toString()); |
| } |
| |
| if (dueDate != null && installmentNumber != null) { |
| throwLoanChargeRefundException("loan.charge.refund.dueDate.and.installmentNumber.provided.use.only.one", installmentNumber, |
| dueDate); |
| } |
| |
| if (loanCharge.isInstalmentFee()) { // identify specific installment |
| if (dueDate == null && installmentNumber == null) { |
| throwLoanChargeRefundException( |
| "loan.charge.refund.neither.dueDate.nor.installmentNumber.provided.for.this.installment.charge", installmentNumber, |
| dueDate); |
| } |
| |
| if (dueDate != null) { |
| installmentChargeEntry = loanCharge.getInstallmentLoanCharge(dueDate); |
| } else if (installmentNumber != null) { |
| installmentChargeEntry = loanCharge.getInstallmentLoanCharge(installmentNumber); |
| } |
| |
| if (installmentChargeEntry == null) { |
| throwLoanChargeRefundException("loan.charge.refund.installment.not.found", installmentNumber, dueDate); |
| } |
| } else { |
| |
| if (dueDate != null || installmentNumber != null) { |
| throwLoanChargeRefundException( |
| "loan.charge.refund.dueDate.or.installmentNumber.provided.but.this.is.not.an.installment.charge", installmentNumber, |
| dueDate); |
| } |
| } |
| |
| return installmentChargeEntry; |
| } |
| |
| private void loanChargeAdjustmentEntranceValidation(final LoanCharge loanCharge, final BigDecimal transactionAmount) { |
| final Loan loan = loanCharge.getLoan(); |
| if (!(loan.isOpen() || loan.getStatus().isClosedObligationsMet() || loan.getStatus().isOverpaid())) { |
| final String errorCode = "loan.charge.adjustment.invalid.status"; |
| throw new LoanChargeAdjustmentException(errorCode, |
| "Adjustment is not supported for the status of " + loan.getStatus().toString()); |
| } |
| |
| if (transactionAmount.compareTo(loanCharge.amount()) > 0) { |
| final String errorCode = "loan.charge.adjustment.invalid.amount"; |
| throw new LoanChargeAdjustmentException(errorCode, |
| "Transaction amount cannot be higher than the charge amount: " + loanCharge.amount()); |
| } |
| |
| BigDecimal availableAmountForAdjustment = calculateAvailableAmountForChargeAdjustment(loanCharge); |
| if (transactionAmount.compareTo(availableAmountForAdjustment) > 0) { |
| final String errorCode = "loan.charge.adjustment.invalid.amount"; |
| throw new LoanChargeAdjustmentException(errorCode, |
| "Transaction amount cannot be higher than the available charge amount for adjustment: " + availableAmountForAdjustment); |
| } |
| checkClientOrGroupActive(loan); |
| loan.validateAccountStatus(LoanEvent.LOAN_CHARGE_ADJUSTMENT); |
| } |
| |
| private BigDecimal calculateAvailableAmountForChargeAdjustment(final LoanCharge loanCharge) { |
| BigDecimal availableAmountForAdjustment = loanCharge.amount(); |
| for (LoanTransaction loanTransaction : loanCharge.getLoan().getLoanTransactions()) { |
| if (loanTransaction.isNotReversed() && loanTransaction.getTypeOf().isChargeAdjustment()) { |
| LoanTransactionRelation loanTransactionRelation = loanTransaction.getLoanTransactionRelations().stream() |
| .filter(e -> e.getToCharge() != null).findFirst().orElseThrow(); |
| if (loanCharge.equals(loanTransactionRelation.getToCharge())) { |
| availableAmountForAdjustment = availableAmountForAdjustment.subtract(loanTransaction.getAmount()); |
| } |
| } |
| } |
| return availableAmountForAdjustment; |
| } |
| |
| private void throwLoanChargeRefundException(String errorMessage, Integer installmentNumber, LocalDate dueDate) { |
| String dueDateValue = ""; |
| String installmentNumberValue = ""; |
| if (dueDate != null) { |
| dueDateValue = dueDate.toString(); |
| } |
| if (installmentNumber != null) { |
| installmentNumberValue = installmentNumber.toString(); |
| } |
| throw new LoanChargeRefundException(errorMessage, "dueDate: " + dueDateValue + " installmentNumber: " + installmentNumberValue); |
| } |
| |
| 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()); |
| } |
| } |
| } |
| } |