diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java index edbb8f892..ac4955d79 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java @@ -3672,6 +3672,22 @@ public class CommandWrapperBuilder { return this; } + public CommandWrapperBuilder reAmortize(final Long loanId) { + this.actionName = "REAMORTIZE"; + this.entityName = "LOAN"; + this.loanId = loanId; + this.href = "/loans/" + loanId + "/transactions?command=reAmortize"; + return this; + } + + public CommandWrapperBuilder undoReAmortize(final Long loanId) { + this.actionName = "UNDO_REAMORTIZE"; + this.entityName = "LOAN"; + this.loanId = loanId; + this.href = "/loans/" + loanId + "/transactions?command=undoReAmortize"; + return this; + } + public CommandWrapperBuilder createDelinquencyAction(final Long loanId) { this.actionName = "CREATE"; this.entityName = "DELINQUENCY_ACTION"; diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanReAmortizationApiConstants.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanReAmortizationApiConstants.java new file mode 100644 index 000000000..5ef37442e --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanReAmortizationApiConstants.java @@ -0,0 +1,26 @@ +/** + * 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.api; + +public interface LoanReAmortizationApiConstants { + + String localeParameterName = "locale"; + String dateFormatParameterName = "dateFormat"; + String externalIdParameterName = "externalId"; +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionEnumData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionEnumData.java index c3985e489..603d7b989 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionEnumData.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionEnumData.java @@ -57,6 +57,7 @@ public class LoanTransactionEnumData { private final boolean chargeoff; private final boolean downPayment; private final boolean reAge; + private final boolean reAmortize; public LoanTransactionEnumData(final Long id, final String code, final String value) { this.id = id; @@ -88,6 +89,7 @@ public class LoanTransactionEnumData { this.chargeoff = Long.valueOf(27).equals(this.id); this.downPayment = Long.valueOf(28).equals(this.id); this.reAge = Long.valueOf(LoanTransactionType.REAGE.getValue()).equals(this.id); + this.reAmortize = Long.valueOf(LoanTransactionType.REAMORTIZE.getValue()).equals(this.id); } public boolean isRepaymentType() { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java index 811baeeb2..7df92edab 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java @@ -685,6 +685,10 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom { return getTypeOf().isReAge() && isNotReversed(); } + public boolean isReAmortize() { + return getTypeOf().isReAmortize() && isNotReversed(); + } + public boolean isIdentifiedBy(final Long identifier) { return getId().equals(identifier); } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java index a057300f6..3e3311444 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java @@ -61,7 +61,7 @@ public enum LoanTransactionType { CHARGE_ADJUSTMENT(26, "loanTransactionType.chargeAdjustment"), // CHARGE_OFF(27, "loanTransactionType.chargeOff"), // DOWN_PAYMENT(28, "loanTransactionType.downPayment"), // - REAGE(29, "loanTransactionType.reAge"); + REAGE(29, "loanTransactionType.reAge"), REAMORTIZE(30, "loanTransactionType.reAmortize"); private final Integer value; private final String code; @@ -106,6 +106,7 @@ public enum LoanTransactionType { case 27 -> LoanTransactionType.CHARGE_OFF; case 28 -> LoanTransactionType.DOWN_PAYMENT; case 29 -> LoanTransactionType.REAGE; + case 30 -> LoanTransactionType.REAMORTIZE; default -> LoanTransactionType.INVALID; }; } @@ -198,6 +199,10 @@ public enum LoanTransactionType { return this.equals(LoanTransactionType.REAGE); } + public boolean isReAmortize() { + return this.equals(LoanTransactionType.REAMORTIZE); + } + public boolean isDownPayment() { return this.equals(LoanTransactionType.DOWN_PAYMENT); } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanEnumerations.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanEnumerations.java index efcae196d..a30de5e9c 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanEnumerations.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanEnumerations.java @@ -315,6 +315,8 @@ public final class LoanEnumerations { LoanTransactionType.DOWN_PAYMENT.getCode(), "Down Payment"); case REAGE -> new LoanTransactionEnumData(LoanTransactionType.REAGE.getValue().longValue(), LoanTransactionType.REAGE.getCode(), "Re-age"); + case REAMORTIZE -> new LoanTransactionEnumData(LoanTransactionType.REAMORTIZE.getValue().longValue(), + LoanTransactionType.REAMORTIZE.getCode(), "Re-amortize"); }; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/reamortization/LoanReAmortizeTransactionBusinessEvent.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/reamortization/LoanReAmortizeTransactionBusinessEvent.java new file mode 100644 index 000000000..4ad0a2d29 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/reamortization/LoanReAmortizeTransactionBusinessEvent.java @@ -0,0 +1,36 @@ +/** + * 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.infrastructure.event.business.domain.loan.transaction.reamortization; + +import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionBusinessEvent; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; + +public class LoanReAmortizeTransactionBusinessEvent extends LoanTransactionBusinessEvent { + + private static final String TYPE = "LoanReAmortizeTransactionBusinessEvent"; + + public LoanReAmortizeTransactionBusinessEvent(LoanTransaction value) { + super(value); + } + + @Override + public String getType() { + return TYPE; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/reamortization/LoanUndoReAmortizeTransactionBusinessEvent.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/reamortization/LoanUndoReAmortizeTransactionBusinessEvent.java new file mode 100644 index 000000000..30cf19e8f --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/reamortization/LoanUndoReAmortizeTransactionBusinessEvent.java @@ -0,0 +1,36 @@ +/** + * 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.infrastructure.event.business.domain.loan.transaction.reamortization; + +import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionBusinessEvent; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; + +public class LoanUndoReAmortizeTransactionBusinessEvent extends LoanTransactionBusinessEvent { + + private static final String TYPE = "LoanUndoReAmortizeTransactionBusinessEvent"; + + public LoanUndoReAmortizeTransactionBusinessEvent(LoanTransaction value) { + super(value); + } + + @Override + public String getType() { + return TYPE; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java index b47944809..98fd50342 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java @@ -83,6 +83,8 @@ public class LoanTransactionsApiResource { public static final String DOWN_PAYMENT = "downPayment"; public static final String UNDO_REAGE = "undoReAge"; public static final String REAGE = "reAge"; + public static final String REAMORTIZE = "reAmortize"; + public static final String UNDO_REAMORTIZE = "undoReAmortize"; private final Set responseDataParameters = new HashSet<>(Arrays.asList("id", "type", "date", "currency", "amount", "externalId", LoanApiConstants.REVERSAL_EXTERNAL_ID_PARAMNAME, LoanApiConstants.REVERSED_ON_DATE_PARAMNAME)); @@ -483,6 +485,10 @@ public class LoanTransactionsApiResource { commandRequest = builder.reAge(resolvedLoanId).build(); } else if (CommandParameterUtil.is(commandParam, UNDO_REAGE)) { commandRequest = builder.undoReAge(resolvedLoanId).build(); + } else if (CommandParameterUtil.is(commandParam, REAMORTIZE)) { + commandRequest = builder.reAmortize(resolvedLoanId).build(); + } else if (CommandParameterUtil.is(commandParam, UNDO_REAMORTIZE)) { + commandRequest = builder.undoReAmortize(resolvedLoanId).build(); } if (commandRequest == null) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/loan/reamortization/LoanReAmortizationCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/loan/reamortization/LoanReAmortizationCommandHandler.java new file mode 100644 index 000000000..50dc537b7 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/loan/reamortization/LoanReAmortizationCommandHandler.java @@ -0,0 +1,50 @@ +/** + * 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.handler.loan.reamortization; + +import lombok.RequiredArgsConstructor; +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.DataIntegrityErrorHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.portfolio.loanaccount.service.reamortization.LoanReAmortizationServiceImpl; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.orm.jpa.JpaSystemException; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@CommandType(entity = "LOAN", action = "REAMORTIZE") +public class LoanReAmortizationCommandHandler implements NewCommandSourceHandler { + + private final LoanReAmortizationServiceImpl loanReAmortizationService; + private final DataIntegrityErrorHandler dataIntegrityErrorHandler; + + @Override + public CommandProcessingResult processCommand(JsonCommand command) { + try { + return loanReAmortizationService.reAmortize(command.getLoanId(), command); + } catch (final JpaSystemException | DataIntegrityViolationException dve) { + dataIntegrityErrorHandler.handleDataIntegrityIssues(command, dve.getMostSpecificCause(), dve, "loan.reAmortize", + "Error while handling re-amortizing"); + return CommandProcessingResult.empty(); + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/loan/reamortization/LoanUndoReAmortizationCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/loan/reamortization/LoanUndoReAmortizationCommandHandler.java new file mode 100644 index 000000000..02012e192 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/loan/reamortization/LoanUndoReAmortizationCommandHandler.java @@ -0,0 +1,50 @@ +/** + * 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.handler.loan.reamortization; + +import lombok.RequiredArgsConstructor; +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.DataIntegrityErrorHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.portfolio.loanaccount.service.reamortization.LoanReAmortizationServiceImpl; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.orm.jpa.JpaSystemException; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@CommandType(entity = "LOAN", action = "UNDO_REAMORTIZE") +public class LoanUndoReAmortizationCommandHandler implements NewCommandSourceHandler { + + private final LoanReAmortizationServiceImpl loanReAmortizationService; + private final DataIntegrityErrorHandler dataIntegrityErrorHandler; + + @Override + public CommandProcessingResult processCommand(JsonCommand command) { + try { + return loanReAmortizationService.undoReAmortize(command.getLoanId(), command); + } catch (final JpaSystemException | DataIntegrityViolationException dve) { + dataIntegrityErrorHandler.handleDataIntegrityIssues(command, dve.getMostSpecificCause(), dve, "loan.undoReAmortize", + "Error while handling undo re-amortizing"); + return CommandProcessingResult.empty(); + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/listener/LoanTransactionDelinquencyRecalculationListener.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/listener/LoanTransactionDelinquencyRecalculationListener.java index 9ee55b142..2d0c27acc 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/listener/LoanTransactionDelinquencyRecalculationListener.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/listener/LoanTransactionDelinquencyRecalculationListener.java @@ -24,6 +24,8 @@ import org.apache.fineract.infrastructure.event.business.BusinessEventListener; import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.reaging.LoanReAgeTransactionBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.reaging.LoanUndoReAgeTransactionBusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.reamortization.LoanReAmortizeTransactionBusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.reamortization.LoanUndoReAmortizeTransactionBusinessEvent; import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; @@ -39,7 +41,9 @@ public class LoanTransactionDelinquencyRecalculationListener // use-cases private static final List> SUPPORTED_EVENT_TYPES = List.of(// LoanReAgeTransactionBusinessEvent.class, // - LoanUndoReAgeTransactionBusinessEvent.class // + LoanUndoReAgeTransactionBusinessEvent.class, // + LoanReAmortizeTransactionBusinessEvent.class, // + LoanUndoReAmortizeTransactionBusinessEvent.class // );// private final LoanAccountDomainService loanAccountDomainService; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reamortization/LoanReAmortizationServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reamortization/LoanReAmortizationServiceImpl.java new file mode 100644 index 000000000..4909adbb1 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reamortization/LoanReAmortizationServiceImpl.java @@ -0,0 +1,140 @@ +/** + * 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.reamortization; + +import static java.math.BigDecimal.ZERO; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; +import org.apache.fineract.infrastructure.core.domain.ExternalId; +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.transaction.reamortization.LoanReAmortizeTransactionBusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.reamortization.LoanUndoReAmortizeTransactionBusinessEvent; +import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; +import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.portfolio.loanaccount.api.LoanReAmortizationApiConstants; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +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.service.LoanAssembler; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional +public class LoanReAmortizationServiceImpl { + + private final LoanAssembler loanAssembler; + private final LoanReAmortizationValidator reAmortizationValidator; + private final ExternalIdFactory externalIdFactory; + private final BusinessEventNotifierService businessEventNotifierService; + private final LoanTransactionRepository loanTransactionRepository; + + public CommandProcessingResult reAmortize(Long loanId, JsonCommand command) { + Loan loan = loanAssembler.assembleFrom(loanId); + reAmortizationValidator.validateReAmortize(loan, command); + + Map changes = new LinkedHashMap<>(); + changes.put(LoanReAmortizationApiConstants.localeParameterName, command.locale()); + changes.put(LoanReAmortizationApiConstants.dateFormatParameterName, command.dateFormat()); + + LoanTransaction reAmortizeTransaction = createReAmortizeTransaction(loan, command); + loanTransactionRepository.saveAndFlush(reAmortizeTransaction); + + // delinquency recalculation will be triggered by the event in a decoupled way via a listener + businessEventNotifierService.notifyPostBusinessEvent(new LoanReAmortizeTransactionBusinessEvent(reAmortizeTransaction)); + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .withEntityId(reAmortizeTransaction.getId()) // + .withEntityExternalId(reAmortizeTransaction.getExternalId()) // + .withOfficeId(loan.getOfficeId()) // + .withClientId(loan.getClientId()) // + .withGroupId(loan.getGroupId()) // + .withLoanId(command.getLoanId()) // + .with(changes).build(); + } + + public CommandProcessingResult undoReAmortize(Long loanId, JsonCommand command) { + Loan loan = loanAssembler.assembleFrom(loanId); + reAmortizationValidator.validateUndoReAmortize(loan, command); + + Map changes = new LinkedHashMap<>(); + changes.put(LoanReAmortizationApiConstants.localeParameterName, command.locale()); + changes.put(LoanReAmortizationApiConstants.dateFormatParameterName, command.dateFormat()); + + LoanTransaction reAmortizeTransaction = findLatestNonReversedReAmortizeTransaction(loan); + if (reAmortizeTransaction == null) { + // TODO: when validations implemented; throw exception if there isn't a reamortize transaction available + } + reverseReAmortizeTransaction(reAmortizeTransaction, command); + loanTransactionRepository.saveAndFlush(reAmortizeTransaction); + + // delinquency recalculation will be triggered by the event in a decoupled way via a listener + businessEventNotifierService.notifyPostBusinessEvent(new LoanUndoReAmortizeTransactionBusinessEvent(reAmortizeTransaction)); + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .withEntityId(reAmortizeTransaction.getId()) // + .withEntityExternalId(reAmortizeTransaction.getExternalId()) // + .withOfficeId(loan.getOfficeId()) // + .withClientId(loan.getClientId()) // + .withGroupId(loan.getGroupId()) // + .withLoanId(command.getLoanId()) // + .with(changes).build(); + } + + private void reverseReAmortizeTransaction(LoanTransaction reAmortizeTransaction, JsonCommand command) { + ExternalId reversalExternalId = externalIdFactory.createFromCommand(command, + LoanReAmortizationApiConstants.externalIdParameterName); + reAmortizeTransaction.reverse(reversalExternalId); + reAmortizeTransaction.manuallyAdjustedOrReversed(); + } + + private LoanTransaction findLatestNonReversedReAmortizeTransaction(Loan loan) { + return loan.getLoanTransactions().stream() // + .filter(LoanTransaction::isNotReversed) // + .filter(LoanTransaction::isReAmortize) // + .max(Comparator.comparing(LoanTransaction::getTransactionDate)) // + .orElse(null); + } + + private LoanTransaction createReAmortizeTransaction(Loan loan, JsonCommand command) { + ExternalId txExternalId = externalIdFactory.createFromCommand(command, LoanReAmortizationApiConstants.externalIdParameterName); + + // reamortize transaction date is always the current business date + LocalDate transactionDate = DateUtils.getBusinessLocalDate(); + + // in case of a reamortize transaction, only the outstanding principal amount until the business date is + // considered + Money txPrincipal = loan.getTotalPrincipalOutstandingUntil(transactionDate); + BigDecimal txPrincipalAmount = txPrincipal.getAmount(); + + return new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.REAMORTIZE.getValue(), transactionDate, txPrincipalAmount, + txPrincipalAmount, ZERO, ZERO, ZERO, null, false, null, txExternalId); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reamortization/LoanReAmortizationValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reamortization/LoanReAmortizationValidator.java new file mode 100644 index 000000000..643c26060 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reamortization/LoanReAmortizationValidator.java @@ -0,0 +1,35 @@ +/** + * 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.reamortization; + +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.springframework.stereotype.Component; + +@Component +public class LoanReAmortizationValidator { + + public void validateReAmortize(Loan loan, JsonCommand command) { + // TODO: implement + } + + public void validateUndoReAmortize(Loan loan, JsonCommand command) { + // TODO: implement + } +} diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index 40e78b8d0..5b640f42d 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -156,4 +156,5 @@ + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0137_add_external_event_for_loan_reamortization.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0137_add_external_event_for_loan_reamortization.xml new file mode 100644 index 000000000..15791895e --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0137_add_external_event_for_loan_reamortization.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java index 76e225632..8c30bdad3 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java @@ -99,7 +99,8 @@ public class ExternalEventConfigurationValidationServiceTest { "LoanRescheduledDueAdjustScheduleBusinessEvent", "LoanOwnershipTransferBusinessEvent", "LoanAccountSnapshotBusinessEvent", "LoanTransactionDownPaymentPostBusinessEvent", "LoanTransactionDownPaymentPreBusinessEvent", "LoanAccountDelinquencyPauseChangedBusinessEvent", "LoanAccountCustomSnapshotBusinessEvent", - "LoanReAgeTransactionBusinessEvent", "LoanUndoReAgeTransactionBusinessEvent"); + "LoanReAgeTransactionBusinessEvent", "LoanUndoReAgeTransactionBusinessEvent", "LoanReAmortizeTransactionBusinessEvent", + "LoanUndoReAmortizeTransactionBusinessEvent"); List tenants = Arrays .asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null)); @@ -180,7 +181,8 @@ public class ExternalEventConfigurationValidationServiceTest { "LoanRescheduledDueAdjustScheduleBusinessEvent", "LoanOwnershipTransferBusinessEvent", "LoanAccountSnapshotBusinessEvent", "LoanTransactionDownPaymentPostBusinessEvent", "LoanTransactionDownPaymentPreBusinessEvent", "LoanAccountDelinquencyPauseChangedBusinessEvent", "LoanAccountCustomSnapshotBusinessEvent", - "LoanReAgeTransactionBusinessEvent", "LoanUndoReAgeTransactionBusinessEvent"); + "LoanReAgeTransactionBusinessEvent", "LoanUndoReAgeTransactionBusinessEvent", "LoanReAmortizeTransactionBusinessEvent", + "LoanUndoReAmortizeTransactionBusinessEvent"); List tenants = Arrays .asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null)); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java index 375fdc2c6..a3073f75e 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java @@ -375,10 +375,21 @@ public abstract class BaseLoanIntegrationTest { loanTransactionHelper.reAge(loanId, request); } + protected void reAmortizeLoan(Long loanId) { + PostLoansLoanIdTransactionsRequest request = new PostLoansLoanIdTransactionsRequest(); + request.setDateFormat(DATETIME_PATTERN); + request.setLocale("en"); + loanTransactionHelper.reAmortize(loanId, request); + } + protected void undoReAgeLoan(Long loanId) { loanTransactionHelper.undoReAge(loanId, new PostLoansLoanIdTransactionsRequest()); } + protected void undoReAmortizeLoan(Long loanId) { + loanTransactionHelper.undoReAmortize(loanId, new PostLoansLoanIdTransactionsRequest()); + } + protected void verifyLastClosedBusinessDate(Long loanId, String lastClosedBusinessDate) { GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); Assertions.assertNotNull(loanDetails.getLastClosedBusinessDate()); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java index 2b8b9e1aa..1196b72db 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java @@ -510,6 +510,16 @@ public class ExternalEventConfigurationHelper { loanUndoReAgeTransactionBusinessEvent.put("enabled", false); defaults.add(loanUndoReAgeTransactionBusinessEvent); + Map loanReAmortizeTransactionBusinessEvent = new HashMap<>(); + loanReAmortizeTransactionBusinessEvent.put("type", "LoanReAmortizeTransactionBusinessEvent"); + loanReAmortizeTransactionBusinessEvent.put("enabled", false); + defaults.add(loanReAmortizeTransactionBusinessEvent); + + Map loanUndoReAmortizeTransactionBusinessEvent = new HashMap<>(); + loanUndoReAmortizeTransactionBusinessEvent.put("type", "LoanUndoReAmortizeTransactionBusinessEvent"); + loanUndoReAmortizeTransactionBusinessEvent.put("enabled", false); + defaults.add(loanUndoReAmortizeTransactionBusinessEvent); + return defaults; } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java index e61438cc4..9f06c03f6 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java @@ -593,10 +593,18 @@ public class LoanTransactionHelper extends IntegrationTest { return ok(fineract().loanTransactions.executeLoanTransaction(loanId, request, "reAge")); } + public PostLoansLoanIdTransactionsResponse reAmortize(final Long loanId, final PostLoansLoanIdTransactionsRequest request) { + return ok(fineract().loanTransactions.executeLoanTransaction(loanId, request, "reAmortize")); + } + public PostLoansLoanIdTransactionsResponse undoReAge(final Long loanId, final PostLoansLoanIdTransactionsRequest request) { return ok(fineract().loanTransactions.executeLoanTransaction(loanId, request, "undoReAge")); } + public PostLoansLoanIdTransactionsResponse undoReAmortize(final Long loanId, final PostLoansLoanIdTransactionsRequest request) { + return ok(fineract().loanTransactions.executeLoanTransaction(loanId, request, "undoReAmortize")); + } + public PutChargeTransactionChangesResponse undoWaiveLoanCharge(final Long loanId, final Long transactionId, final PutChargeTransactionChangesRequest request) { log.info("--------------------------------- UNDO WAIVE CHARGES FOR LOAN --------------------------------"); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reamortization/LoanReAmortizationIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reamortization/LoanReAmortizationIntegrationTest.java new file mode 100644 index 000000000..fd3946931 --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reamortization/LoanReAmortizationIntegrationTest.java @@ -0,0 +1,184 @@ +/** + * 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.integrationtests.loan.reamortization; + +import java.math.BigDecimal; +import java.util.concurrent.atomic.AtomicLong; +import org.apache.fineract.client.models.PostLoanProductsRequest; +import org.apache.fineract.client.models.PostLoanProductsResponse; +import org.apache.fineract.client.models.PostLoansLoanIdResponse; +import org.apache.fineract.client.models.PostLoansRequest; +import org.apache.fineract.client.models.PostLoansResponse; +import org.apache.fineract.integrationtests.BaseLoanIntegrationTest; +import org.apache.fineract.integrationtests.common.ClientHelper; +import org.junit.jupiter.api.Test; + +public class LoanReAmortizationIntegrationTest extends BaseLoanIntegrationTest { + + @Test + public void test_LoanReAmortizeTransaction_Works() { + AtomicLong createdLoanId = new AtomicLong(); + + runAt("01 January 2023", () -> { + // Create Client + Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + + int numberOfRepayments = 1; + int repaymentEvery = 1; + + // Create Loan Product + PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct() // + .numberOfRepayments(numberOfRepayments) // + .repaymentEvery(repaymentEvery) // + .repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue()); // + + PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product); + Long loanProductId = loanProductResponse.getResourceId(); + + // Apply and Approve Loan + double amount = 1250.0; + + PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductId, "01 January 2023", amount, numberOfRepayments)// + .repaymentEvery(repaymentEvery)// + .loanTermFrequency(numberOfRepayments)// + .repaymentFrequencyType(RepaymentFrequencyType.MONTHS)// + .loanTermFrequencyType(RepaymentFrequencyType.MONTHS); + + PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applicationRequest); + + PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(), + approveLoanRequest(amount, "01 January 2023")); + + Long loanId = approvedLoanResult.getLoanId(); + + // disburse Loan + disburseLoan(loanId, BigDecimal.valueOf(1250.0), "01 January 2023"); + + // verify transactions + verifyTransactions(loanId, // + transaction(1250.0, "Disbursement", "01 January 2023") // + ); + + // verify schedule + verifyRepaymentSchedule(loanId, // + installment(0, null, "01 January 2023"), // + installment(1250.0, false, "01 February 2023") // + ); + + createdLoanId.set(loanId); + }); + + runAt("02 February 2023", () -> { + long loanId = createdLoanId.get(); + + // create re-amortize transaction + reAmortizeLoan(loanId); + + // verify transactions + verifyTransactions(loanId, // + transaction(1250.0, "Disbursement", "01 January 2023"), // + transaction(1250.0, "Re-amortize", "02 February 2023") // + ); + + // TODO: verify installments when schedule generation is implemented + }); + } + + @Test + public void test_LoanUndoReAmortizeTransaction_Works() { + AtomicLong createdLoanId = new AtomicLong(); + + runAt("01 January 2023", () -> { + // Create Client + Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + + int numberOfRepayments = 1; + int repaymentEvery = 1; + + // Create Loan Product + PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct() // + .numberOfRepayments(numberOfRepayments) // + .repaymentEvery(repaymentEvery) // + .repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue()); // + + PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product); + Long loanProductId = loanProductResponse.getResourceId(); + + // Apply and Approve Loan + double amount = 1250.0; + + PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductId, "01 January 2023", amount, numberOfRepayments)// + .repaymentEvery(repaymentEvery)// + .loanTermFrequency(numberOfRepayments)// + .repaymentFrequencyType(RepaymentFrequencyType.MONTHS)// + .loanTermFrequencyType(RepaymentFrequencyType.MONTHS); + + PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applicationRequest); + + PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(), + approveLoanRequest(amount, "01 January 2023")); + + Long loanId = approvedLoanResult.getLoanId(); + + // disburse Loan + disburseLoan(loanId, BigDecimal.valueOf(1250.0), "01 January 2023"); + + // verify transactions + verifyTransactions(loanId, // + transaction(1250.0, "Disbursement", "01 January 2023") // + ); + + // verify schedule + verifyRepaymentSchedule(loanId, // + installment(0, null, "01 January 2023"), // + installment(1250.0, false, "01 February 2023") // + ); + + createdLoanId.set(loanId); + }); + + runAt("02 February 2023", () -> { + long loanId = createdLoanId.get(); + + // create re-amortize transaction + reAmortizeLoan(loanId); + + // verify transactions + verifyTransactions(loanId, // + transaction(1250.0, "Disbursement", "01 January 2023"), // + transaction(1250.0, "Re-amortize", "02 February 2023") // + ); + }); + + runAt("03 February 2023", () -> { + long loanId = createdLoanId.get(); + + // create re-amortize transaction + undoReAmortizeLoan(loanId); + + // verify transactions + verifyTransactions(loanId, // + transaction(1250.0, "Disbursement", "01 January 2023"), // + reversedTransaction(1250.0, "Re-amortize", "02 February 2023") // + ); + + // TODO: verify installments when schedule generation is implemented + }); + } +}