diff --git a/fineract-avro-schemas/src/main/avro/loan/v1/LoanOwnershipTransferDataV1.avsc b/fineract-avro-schemas/src/main/avro/loan/v1/LoanOwnershipTransferDataV1.avsc new file mode 100644 index 000000000..3b94a4331 --- /dev/null +++ b/fineract-avro-schemas/src/main/avro/loan/v1/LoanOwnershipTransferDataV1.avsc @@ -0,0 +1,158 @@ +{ + "name": "LoanOwnershipTransferDataV1", + "namespace": "org.apache.fineract.avro.loan.v1", + "type": "record", + "fields": [ + { + "name": "loanId", + "type": ["long"] + }, + { + "default": null, + "name": "loanExternalId", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "type", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "transferExternalId", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "submittedDate", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "assetOwnerExternalId", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "currency", + "type": [ + "null", + "org.apache.fineract.avro.generic.v1.CurrencyDataV1" + ] + }, + { + "default": null, + "name": "totalOutstandingBalanceAmount", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "outstandingPrincipalPortion", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "outstandingFeePortion", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "outstandingPenaltyPortion", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "outstandingInterestPortion", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "overPaymentPortion", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "unrecognizedIncomePortion", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "unpaidChargeData", + "type": [ + "null", + { + "type": "array", + "items": "org.apache.fineract.avro.loan.v1.UnpaidChargeDataV1" + } + ] + }, + { + "default": null, + "name": "transferStatus", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "transferStatusReason", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "settlementDate", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "purchasePriceRatio", + "type": [ + "null", + "string" + ] + } + ] +} diff --git a/fineract-core/dependencies.gradle b/fineract-core/dependencies.gradle index c94a32a0e..305607ae0 100644 --- a/fineract-core/dependencies.gradle +++ b/fineract-core/dependencies.gradle @@ -28,7 +28,6 @@ dependencies { ) // implementation dependencies are directly used (compiled against) in src/main (and src/test) - // implementation( 'org.springframework.boot:spring-boot-starter-web', 'org.springframework.boot:spring-boot-starter-security', @@ -127,4 +126,8 @@ dependencies { exclude group: 'com.zaxxer', module: 'HikariCP-java7' } testImplementation ('org.mockito:mockito-inline') + implementation('org.apache.avro:avro') + implementation( + project(path: ':fineract-avro-schemas') + ) } diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/config/MapstructMapperConfig.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/config/MapstructMapperConfig.java index 705d5f6a6..30938deff 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/config/MapstructMapperConfig.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/config/MapstructMapperConfig.java @@ -20,10 +20,11 @@ package org.apache.fineract.infrastructure.core.config; import org.apache.fineract.infrastructure.event.external.service.serialization.mapper.support.ExternalIdMapper; import org.mapstruct.Builder; +import org.mapstruct.InjectionStrategy; import org.mapstruct.MapperConfig; import org.mapstruct.MappingConstants; import org.mapstruct.ReportingPolicy; @MapperConfig(componentModel = MappingConstants.ComponentModel.SPRING, unmappedTargetPolicy = ReportingPolicy.ERROR, builder = @Builder(disableBuilder = true), uses = { - ExternalIdMapper.class }) + ExternalIdMapper.class }, injectionStrategy = InjectionStrategy.CONSTRUCTOR) public class MapstructMapperConfig {} diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/mapper/support/AvroMapperConfig.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/mapper/support/AvroMapperConfig.java index 7de3ca674..8f297aac9 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/mapper/support/AvroMapperConfig.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/mapper/support/AvroMapperConfig.java @@ -19,10 +19,11 @@ package org.apache.fineract.infrastructure.event.external.service.serialization.mapper.support; import org.mapstruct.Builder; +import org.mapstruct.InjectionStrategy; import org.mapstruct.MapperConfig; import org.mapstruct.MappingConstants; import org.mapstruct.ReportingPolicy; @MapperConfig(componentModel = MappingConstants.ComponentModel.SPRING, unmappedTargetPolicy = ReportingPolicy.ERROR, builder = @Builder(disableBuilder = true), uses = { - AvroDateTimeMapper.class, AvroMonthDayMapper.class, ExternalIdMapper.class }) + AvroDateTimeMapper.class, AvroMonthDayMapper.class, ExternalIdMapper.class }, injectionStrategy = InjectionStrategy.CONSTRUCTOR) public class AvroMapperConfig {} diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStep.java b/fineract-investor/src/main/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStep.java index 34752d86a..ed26fe3de 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStep.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStep.java @@ -26,6 +26,8 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.fineract.cob.loan.LoanCOBBusinessStep; import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.event.business.domain.loan.LoanAccountSnapshotBusinessEvent; +import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; import org.apache.fineract.investor.config.InvestorModuleIsEnabledCondition; import org.apache.fineract.investor.data.ExternalTransferStatus; import org.apache.fineract.investor.data.ExternalTransferSubStatus; @@ -34,6 +36,7 @@ import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferDetails; import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMapping; import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMappingRepository; import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferRepository; +import org.apache.fineract.investor.domain.LoanOwnershipTransferBusinessEvent; import org.apache.fineract.investor.service.AccountingService; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.springframework.context.annotation.Conditional; @@ -50,6 +53,7 @@ public class LoanAccountOwnerTransferBusinessStep implements LoanCOBBusinessStep private final ExternalAssetOwnerTransferRepository externalAssetOwnerTransferRepository; private final ExternalAssetOwnerTransferLoanMappingRepository externalAssetOwnerTransferLoanMappingRepository; private final AccountingService accountingService; + private final BusinessEventNotifierService businessEventNotifierService; @Override public Loan execute(Loan loan) { @@ -73,7 +77,7 @@ public class LoanAccountOwnerTransferBusinessStep implements LoanCOBBusinessStep throw new IllegalStateException(String.format("Illegal transfer found. Expected %s and %s, found: %s and %s", ExternalTransferStatus.PENDING, ExternalTransferStatus.BUYBACK, firstTransferStatus, secondTransferStatus)); } - handleSameDaySaleAndBuyback(settlementDate, transferDataList); + handleSameDaySaleAndBuyback(settlementDate, transferDataList, loan); } else if (size == 1) { ExternalAssetOwnerTransfer transfer = transferDataList.get(0); if (ExternalTransferStatus.PENDING.equals(transfer.getStatus())) { @@ -89,7 +93,8 @@ public class LoanAccountOwnerTransferBusinessStep implements LoanCOBBusinessStep private void handleSale(final Loan loan, final LocalDate settlementDate, final ExternalAssetOwnerTransfer externalAssetOwnerTransfer) { ExternalAssetOwnerTransfer newExternalAssetOwnerTransfer = sellAsset(loan, settlementDate, externalAssetOwnerTransfer); - // TODO: trigger asset loan transfer executed event + businessEventNotifierService.notifyPostBusinessEvent(new LoanOwnershipTransferBusinessEvent(newExternalAssetOwnerTransfer, loan)); + businessEventNotifierService.notifyPostBusinessEvent(new LoanAccountSnapshotBusinessEvent(loan)); } private void handleBuyback(final Loan loan, final LocalDate settlementDate, @@ -102,7 +107,8 @@ public class LoanAccountOwnerTransferBusinessStep implements LoanCOBBusinessStep .orElseThrow(); ExternalAssetOwnerTransfer newExternalAssetOwnerTransfer = buybackAsset(loan, settlementDate, buybackExternalAssetOwnerTransfer, activeExternalAssetOwnerTransfer); - // TODO: trigger asset loan transfer executed event + businessEventNotifierService.notifyPostBusinessEvent(new LoanOwnershipTransferBusinessEvent(newExternalAssetOwnerTransfer, loan)); + businessEventNotifierService.notifyPostBusinessEvent(new LoanAccountSnapshotBusinessEvent(loan)); } private ExternalAssetOwnerTransfer buybackAsset(final Loan loan, final LocalDate settlementDate, @@ -166,10 +172,13 @@ public class LoanAccountOwnerTransferBusinessStep implements LoanCOBBusinessStep return loan.getLoanSummary().getTotalOutstanding().compareTo(BigDecimal.ZERO) > 0; } - private void handleSameDaySaleAndBuyback(final LocalDate settlementDate, final List transferDataList) { + private void handleSameDaySaleAndBuyback(final LocalDate settlementDate, final List transferDataList, + Loan loan) { ExternalAssetOwnerTransfer cancelledPendingTransfer = cancelTransfer(settlementDate, transferDataList.get(0)); ExternalAssetOwnerTransfer cancelledBuybackTransfer = cancelTransfer(settlementDate, transferDataList.get(1)); - // TODO: trigger asset loan transfer cancel events + businessEventNotifierService.notifyPostBusinessEvent(new LoanOwnershipTransferBusinessEvent(cancelledPendingTransfer, loan)); + businessEventNotifierService.notifyPostBusinessEvent(new LoanOwnershipTransferBusinessEvent(cancelledBuybackTransfer, loan)); + businessEventNotifierService.notifyPostBusinessEvent(new LoanAccountSnapshotBusinessEvent(loan)); } private ExternalAssetOwnerTransfer cancelTransfer(final LocalDate settlementDate, diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/domain/InvestorBusinessEvent.java b/fineract-investor/src/main/java/org/apache/fineract/investor/domain/InvestorBusinessEvent.java new file mode 100644 index 000000000..76ccb5b2e --- /dev/null +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/domain/InvestorBusinessEvent.java @@ -0,0 +1,45 @@ +/** + * 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.investor.domain; + +import lombok.Getter; +import org.apache.fineract.infrastructure.event.business.domain.AbstractBusinessEvent; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; + +@Getter +public abstract class InvestorBusinessEvent extends AbstractBusinessEvent { + + private final Loan loan; + private static final String CATEGORY = "Investor"; + + public InvestorBusinessEvent(ExternalAssetOwnerTransfer value, Loan loan) { + super(value); + this.loan = loan; + } + + @Override + public String getCategory() { + return CATEGORY; + } + + @Override + public Long getAggregateRootId() { + return get().getId(); + } +} diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/domain/LoanOwnershipTransferBusinessEvent.java b/fineract-investor/src/main/java/org/apache/fineract/investor/domain/LoanOwnershipTransferBusinessEvent.java new file mode 100644 index 000000000..b6aa67068 --- /dev/null +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/domain/LoanOwnershipTransferBusinessEvent.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.investor.domain; + +import org.apache.fineract.portfolio.loanaccount.domain.Loan; + +public class LoanOwnershipTransferBusinessEvent extends InvestorBusinessEvent { + + private static final String TYPE = "LoanOwnershipTransferBusinessEvent"; + + public LoanOwnershipTransferBusinessEvent(ExternalAssetOwnerTransfer value, Loan loan) { + super(value, loan); + } + + @Override + public String getType() { + return TYPE; + } +} diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/exception/ExternalAssetOwnerTransferNotFoundException.java b/fineract-investor/src/main/java/org/apache/fineract/investor/exception/ExternalAssetOwnerTransferNotFoundException.java index 985517a4d..05e8e3654 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/exception/ExternalAssetOwnerTransferNotFoundException.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/exception/ExternalAssetOwnerTransferNotFoundException.java @@ -30,4 +30,9 @@ public class ExternalAssetOwnerTransferNotFoundException extends AbstractPlatfor externalTransferStatus), externalId.getValue(), externalTransferStatus); } + + public ExternalAssetOwnerTransferNotFoundException(Long id) { + super("error.msg.external.asset.owner.transfer.id", String.format("External asset owner transfer with id: %s does not found", id), + id); + } } diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersReadService.java b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersReadService.java index d6a8c33da..859004681 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersReadService.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersReadService.java @@ -28,6 +28,8 @@ public interface ExternalAssetOwnersReadService { Page retrieveTransferData(Long loanId, String externalLoanId, String transferExternalId, Integer offset, Integer limit); + ExternalTransferData retrieveTransferData(Long transferExternalId); + ExternalTransferData retrieveActiveTransferData(Long loanId, String externalLoanId, String transferExternalId); ExternalOwnerTransferJournalEntryData retrieveJournalEntriesOfTransfer(Long transferId, Integer offset, Integer limit); diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersReadServiceImpl.java b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersReadServiceImpl.java index bf619e275..ca9d18ad7 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersReadServiceImpl.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersReadServiceImpl.java @@ -33,6 +33,7 @@ import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferJournalEntr import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMapping; import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMappingRepository; import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferRepository; +import org.apache.fineract.investor.exception.ExternalAssetOwnerTransferNotFoundException; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; @@ -122,4 +123,10 @@ public class ExternalAssetOwnersReadServiceImpl implements ExternalAssetOwnersRe return PageRequest.of(offset, limit, Sort.by("id")); } + @Override + public ExternalTransferData retrieveTransferData(Long externalTransferId) { + return externalAssetOwnerTransferRepository.findById(externalTransferId).map(mapper::mapTransfer) + .orElseThrow(() -> new ExternalAssetOwnerTransferNotFoundException(externalTransferId)); + } + } diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/service/serialization/serializer/investor/InvestorBusinessEventSerializer.java b/fineract-investor/src/main/java/org/apache/fineract/investor/service/serialization/serializer/investor/InvestorBusinessEventSerializer.java new file mode 100644 index 000000000..68b5c2af0 --- /dev/null +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/service/serialization/serializer/investor/InvestorBusinessEventSerializer.java @@ -0,0 +1,145 @@ +/** + * 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.investor.service.serialization.serializer.investor; + +import static org.apache.fineract.infrastructure.core.service.DateUtils.DEFAULT_DATE_FORMATTER; +import static org.apache.fineract.investor.data.ExternalTransferStatus.ACTIVE; +import static org.apache.fineract.investor.data.ExternalTransferStatus.BUYBACK; +import static org.apache.fineract.investor.data.ExternalTransferStatus.CANCELLED; +import static org.apache.fineract.investor.data.ExternalTransferStatus.DECLINED; +import static org.apache.fineract.investor.data.ExternalTransferSubStatus.BALANCE_NEGATIVE; +import static org.apache.fineract.investor.data.ExternalTransferSubStatus.BALANCE_ZERO; +import static org.apache.fineract.investor.data.ExternalTransferSubStatus.SAMEDAY_TRANSFERS; + +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.apache.avro.generic.GenericContainer; +import org.apache.fineract.avro.generator.ByteBufferSerializable; +import org.apache.fineract.avro.generic.v1.CurrencyDataV1; +import org.apache.fineract.avro.loan.v1.LoanOwnershipTransferDataV1; +import org.apache.fineract.avro.loan.v1.UnpaidChargeDataV1; +import org.apache.fineract.infrastructure.event.business.domain.BusinessEvent; +import org.apache.fineract.infrastructure.event.external.service.serialization.serializer.BusinessEventSerializer; +import org.apache.fineract.investor.data.ExternalTransferData; +import org.apache.fineract.investor.data.ExternalTransferStatus; +import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer; +import org.apache.fineract.investor.domain.InvestorBusinessEvent; +import org.apache.fineract.investor.service.ExternalAssetOwnersReadService; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class InvestorBusinessEventSerializer implements BusinessEventSerializer { + + private final ExternalAssetOwnersReadService externalAssetOwnersReadService; + + private static CurrencyDataV1 getCurrencyFromEvent(InvestorBusinessEvent event) { + MonetaryCurrency loanCurrency = event.getLoan().getCurrency(); + CurrencyDataV1 currency = CurrencyDataV1.newBuilder().setCode(loanCurrency.getCode()) + .setDecimalPlaces(loanCurrency.getDigitsAfterDecimal()).setInMultiplesOf(loanCurrency.getCurrencyInMultiplesOf()).build(); + return currency; + } + + @Override + public boolean canSerialize(BusinessEvent event) { + return event instanceof InvestorBusinessEvent; + } + + @Override + public Class getSupportedSchema() { + return LoanOwnershipTransferDataV1.class; + } + + @Override + public ByteBufferSerializable toAvroDTO(BusinessEvent rawEvent) { + InvestorBusinessEvent event = (InvestorBusinessEvent) rawEvent; + ExternalTransferData transferData = externalAssetOwnersReadService.retrieveTransferData(event.get().getId()); + + LoanOwnershipTransferDataV1.Builder builder = LoanOwnershipTransferDataV1.newBuilder().setLoanId(transferData.getLoan().getLoanId()) + .setLoanExternalId(transferData.getLoan().getExternalId()).setTransferExternalId(transferData.getTransferExternalId()) + .setAssetOwnerExternalId(transferData.getOwner().getExternalId()) + .setPurchasePriceRatio(transferData.getPurchasePriceRatio()).setCurrency(getCurrencyFromEvent(event)) + .setSettlementDate(transferData.getSettlementDate().format(DEFAULT_DATE_FORMATTER)) + .setSubmittedDate(transferData.getSettlementDate().format(DEFAULT_DATE_FORMATTER)) + .setType(transferData.getStatus() == BUYBACK ? "BUYBACK" : "SALE").setTransferStatus(getStatus(event.get())) + .setTransferStatusReason(getTransferStatusReason(event.get()).orElse(null)); + + if (transferData.getDetails() != null) { + builder.setTotalOutstandingBalanceAmount(transferData.getDetails().getTotalOutstanding()) + .setOutstandingPrincipalPortion(transferData.getDetails().getTotalPrincipalOutstanding()) + .setOutstandingInterestPortion(transferData.getDetails().getTotalInterestOutstanding()) + .setOutstandingFeePortion(transferData.getDetails().getTotalFeeChargesOutstanding()) + .setOutstandingPenaltyPortion(transferData.getDetails().getTotalPenaltyChargesOutstanding()) + .setUnpaidChargeData(getUnpaidChargeData(event)).setOverPaymentPortion(transferData.getDetails().getTotalOverpaid()); + } + + return builder.build(); + } + + private List getUnpaidChargeData(InvestorBusinessEvent event) { + java.util.Map map = new HashMap<>(); + event.getLoan().getLoanCharges().forEach(loanCharge -> addToMap(map, loanCharge)); + return map.values().stream().toList(); + } + + private void addToMap(Map map, LoanCharge loanCharge) { + if (loanCharge.amountOutstanding().compareTo(BigDecimal.ZERO) > 0) { + UnpaidChargeDataV1 toAdd = new UnpaidChargeDataV1(loanCharge.getCharge().getId(), loanCharge.name(), + loanCharge.amountOutstanding()); + UnpaidChargeDataV1 unpaidChargeDataV1 = map.get(loanCharge.getCharge().getId()); + if (unpaidChargeDataV1 == null) { + map.put(toAdd.getChargeId(), toAdd); + } else { + unpaidChargeDataV1.setOutstandingAmount(unpaidChargeDataV1.getOutstandingAmount().add(toAdd.getOutstandingAmount())); + } + } + } + + private String getStatus(ExternalAssetOwnerTransfer externalAssetOwnerTransfer) { + ExternalTransferStatus status = externalAssetOwnerTransfer.getStatus(); + if (ACTIVE.equals(status) || BUYBACK.equals(status)) { + return "EXECUTED"; + } else if (DECLINED.equals(status)) { + return "DECLINED"; + } else if (CANCELLED.equals(status)) { + return "CANCELLED"; + } else { + return "UNKNOWN"; + } + } + + private Optional getTransferStatusReason(ExternalAssetOwnerTransfer externalAssetOwnerTransfer) { + ExternalTransferStatus status = externalAssetOwnerTransfer.getStatus(); + if (DECLINED.equals(status) && BALANCE_ZERO.equals(externalAssetOwnerTransfer.getSubStatus())) { + return Optional.of("BALANCE ZERO"); + } else if (DECLINED.equals(status) && BALANCE_NEGATIVE.equals(externalAssetOwnerTransfer.getSubStatus())) { + return Optional.of("BALANCE NEGATIV"); + } else if (CANCELLED.equals(status) && SAMEDAY_TRANSFERS.equals(externalAssetOwnerTransfer.getSubStatus())) { + return Optional.of("SAMEDAY TRANSFER"); + } else { + return Optional.empty(); + } + } +} diff --git a/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/module-changelog-master.xml b/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/module-changelog-master.xml index a6cb411e3..afdc51ec0 100644 --- a/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/module-changelog-master.xml +++ b/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/module-changelog-master.xml @@ -30,4 +30,5 @@ + diff --git a/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/parts/0009_add_loan_ownership_transfer_events.xml b/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/parts/0009_add_loan_ownership_transfer_events.xml new file mode 100644 index 000000000..6395fc13b --- /dev/null +++ b/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/parts/0009_add_loan_ownership_transfer_events.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + diff --git a/fineract-investor/src/test/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStepTest.java b/fineract-investor/src/test/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStepTest.java index 171b0bbb5..60cef35a5 100644 --- a/fineract-investor/src/test/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStepTest.java +++ b/fineract-investor/src/test/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStepTest.java @@ -21,10 +21,12 @@ package org.apache.fineract.investor.cob.loan; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import java.math.BigDecimal; @@ -38,15 +40,20 @@ import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; import org.apache.fineract.infrastructure.core.domain.ActionContext; import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant; import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; +import org.apache.fineract.infrastructure.event.business.domain.BusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.loan.LoanAccountSnapshotBusinessEvent; +import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; import org.apache.fineract.investor.data.ExternalTransferStatus; import org.apache.fineract.investor.data.ExternalTransferSubStatus; import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer; import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMapping; import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMappingRepository; import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferRepository; +import org.apache.fineract.investor.domain.LoanOwnershipTransferBusinessEvent; import org.apache.fineract.investor.service.AccountingService; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanSummary; +import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -66,6 +73,10 @@ public class LoanAccountOwnerTransferBusinessStepTest { private ExternalAssetOwnerTransferRepository externalAssetOwnerTransferRepository; @Mock private ExternalAssetOwnerTransferLoanMappingRepository externalAssetOwnerTransferLoanMappingRepository; + + @Mock + private BusinessEventNotifierService businessEventNotifierService; + @Mock private AccountingService accountingService; private LoanAccountOwnerTransferBusinessStep underTest; @@ -76,7 +87,7 @@ public class LoanAccountOwnerTransferBusinessStepTest { ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT); ThreadLocalContextUtil.setBusinessDates(new HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, actualDate))); underTest = new LoanAccountOwnerTransferBusinessStep(externalAssetOwnerTransferRepository, - externalAssetOwnerTransferLoanMappingRepository, accountingService); + externalAssetOwnerTransferLoanMappingRepository, accountingService, businessEventNotifierService); } @Test @@ -89,6 +100,7 @@ public class LoanAccountOwnerTransferBusinessStepTest { final Loan processedLoan = underTest.execute(loanForProcessing); // then verify(externalAssetOwnerTransferRepository, times(1)).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id"))); + verifyNoInteractions(businessEventNotifierService); assertEquals(processedLoan, loanForProcessing); } @@ -109,6 +121,7 @@ public class LoanAccountOwnerTransferBusinessStepTest { // then assertEquals("Illegal transfer found. Expected PENDING and BUYBACK, found: PENDING and ACTIVE", exception.getMessage()); verify(externalAssetOwnerTransferRepository, times(1)).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id"))); + verifyNoInteractions(businessEventNotifierService); } @Test @@ -118,6 +131,15 @@ public class LoanAccountOwnerTransferBusinessStepTest { when(loanForProcessing.getId()).thenReturn(1L); ExternalAssetOwnerTransfer firstResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class); ExternalAssetOwnerTransfer secondResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class); + + ExternalAssetOwnerTransfer firstSaveResult = Mockito.mock(ExternalAssetOwnerTransfer.class); + ExternalAssetOwnerTransfer secondSaveResult = Mockito.mock(ExternalAssetOwnerTransfer.class); + ExternalAssetOwnerTransfer thirdSaveResult = Mockito.mock(ExternalAssetOwnerTransfer.class); + ExternalAssetOwnerTransfer fourthSaveResult = Mockito.mock(ExternalAssetOwnerTransfer.class); + + when(externalAssetOwnerTransferRepository.save(any(ExternalAssetOwnerTransfer.class))).thenReturn(firstSaveResult) + .thenReturn(secondSaveResult).thenReturn(thirdSaveResult).thenReturn(fourthSaveResult); + when(firstResponseItem.getStatus()).thenReturn(ExternalTransferStatus.PENDING); when(secondResponseItem.getStatus()).thenReturn(ExternalTransferStatus.BUYBACK); List response = List.of(firstResponseItem, secondResponseItem); @@ -163,6 +185,11 @@ public class LoanAccountOwnerTransferBusinessStepTest { assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(3).getEffectiveDateTo()); assertEquals(processedLoan, loanForProcessing); + + ArgumentCaptor> businessEventArgumentCaptor = verifyBusinessEvents(3); + verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, secondSaveResult); + verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 1, loanForProcessing, fourthSaveResult); + verifyLoanAccountSnapshotBusinessEvent(businessEventArgumentCaptor, 2, loanForProcessing); } @Test @@ -193,6 +220,10 @@ public class LoanAccountOwnerTransferBusinessStepTest { verify(externalAssetOwnerTransferLoanMappingRepository, times(1)).deleteByLoanIdAndOwnerTransfer(1L, secondResponseItem); assertEquals(processedLoan, loanForProcessing); + + ArgumentCaptor> businessEventArgumentCaptor = verifyBusinessEvents(2); + verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, firstResponseItem); + verifyLoanAccountSnapshotBusinessEvent(businessEventArgumentCaptor, 1, loanForProcessing); } @Test @@ -238,6 +269,10 @@ public class LoanAccountOwnerTransferBusinessStepTest { assertEquals(1L, externalAssetOwnerTransferLoanMappingArgumentCaptor.getValue().getLoanId()); assertEquals(newTransfer, externalAssetOwnerTransferLoanMappingArgumentCaptor.getValue().getOwnerTransfer()); assertEquals(processedLoan, loanForProcessing); + + ArgumentCaptor> businessEventArgumentCaptor = verifyBusinessEvents(2); + verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, newTransfer); + verifyLoanAccountSnapshotBusinessEvent(businessEventArgumentCaptor, 1, loanForProcessing); } @Test @@ -278,6 +313,10 @@ public class LoanAccountOwnerTransferBusinessStepTest { assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getEffectiveDateFrom()); assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getEffectiveDateTo()); assertEquals(processedLoan, loanForProcessing); + + ArgumentCaptor> businessEventArgumentCaptor = verifyBusinessEvents(2); + verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, newTransfer); + verifyLoanAccountSnapshotBusinessEvent(businessEventArgumentCaptor, 1, loanForProcessing); } @Test @@ -318,6 +357,10 @@ public class LoanAccountOwnerTransferBusinessStepTest { assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getEffectiveDateFrom()); assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getEffectiveDateTo()); assertEquals(processedLoan, loanForProcessing); + + ArgumentCaptor> businessEventArgumentCaptor = verifyBusinessEvents(2); + verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, newTransfer); + verifyLoanAccountSnapshotBusinessEvent(businessEventArgumentCaptor, 1, loanForProcessing); } @Test @@ -333,4 +376,26 @@ public class LoanAccountOwnerTransferBusinessStepTest { assertNotNull(actualEnumName); assertEquals("Execute external asset owner transfer", actualEnumName); } + + @NotNull + private ArgumentCaptor> verifyBusinessEvents(int expectedBusinessEvents) { + @SuppressWarnings("unchecked") + ArgumentCaptor> businessEventArgumentCaptor = ArgumentCaptor.forClass(BusinessEvent.class); + verify(businessEventNotifierService, times(expectedBusinessEvents)).notifyPostBusinessEvent(businessEventArgumentCaptor.capture()); + return businessEventArgumentCaptor; + } + + private void verifyLoanTransferBusinessEvent(ArgumentCaptor> businessEventArgumentCaptor, int index, Loan expectedLoan, + ExternalAssetOwnerTransfer expectedAssetOwnerTransfer) { + assertTrue(businessEventArgumentCaptor.getAllValues().get(index) instanceof LoanOwnershipTransferBusinessEvent); + assertEquals(expectedLoan, ((LoanOwnershipTransferBusinessEvent) businessEventArgumentCaptor.getAllValues().get(index)).getLoan()); + assertEquals(expectedAssetOwnerTransfer, + ((LoanOwnershipTransferBusinessEvent) businessEventArgumentCaptor.getAllValues().get(index)).get()); + } + + private void verifyLoanAccountSnapshotBusinessEvent(ArgumentCaptor> businessEventArgumentCaptor, int index, + Loan expectedLoan) { + assertTrue(businessEventArgumentCaptor.getAllValues().get(index) instanceof LoanAccountSnapshotBusinessEvent); + assertEquals(expectedLoan, ((LoanAccountSnapshotBusinessEvent) businessEventArgumentCaptor.getAllValues().get(index)).get()); + } } diff --git a/fineract-investor/src/test/java/org/apache/fineract/investor/service/serialization/serializer/investor/InvestorBusinessEventSerializerTest.java b/fineract-investor/src/test/java/org/apache/fineract/investor/service/serialization/serializer/investor/InvestorBusinessEventSerializerTest.java new file mode 100644 index 000000000..201f7863d --- /dev/null +++ b/fineract-investor/src/test/java/org/apache/fineract/investor/service/serialization/serializer/investor/InvestorBusinessEventSerializerTest.java @@ -0,0 +1,200 @@ +/** + * 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.investor.service.serialization.serializer.investor; + +import static org.apache.fineract.investor.data.ExternalTransferStatus.ACTIVE; +import static org.apache.fineract.investor.data.ExternalTransferStatus.BUYBACK; +import static org.apache.fineract.investor.data.ExternalTransferStatus.CANCELLED; +import static org.apache.fineract.investor.data.ExternalTransferStatus.DECLINED; +import static org.apache.fineract.investor.data.ExternalTransferSubStatus.BALANCE_NEGATIVE; +import static org.apache.fineract.investor.data.ExternalTransferSubStatus.BALANCE_ZERO; +import static org.apache.fineract.investor.data.ExternalTransferSubStatus.SAMEDAY_TRANSFERS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.apache.fineract.avro.generator.ByteBufferSerializable; +import org.apache.fineract.avro.loan.v1.LoanOwnershipTransferDataV1; +import org.apache.fineract.avro.loan.v1.UnpaidChargeDataV1; +import org.apache.fineract.investor.data.ExternalTransferData; +import org.apache.fineract.investor.data.ExternalTransferDataDetails; +import org.apache.fineract.investor.data.ExternalTransferLoanData; +import org.apache.fineract.investor.data.ExternalTransferOwnerData; +import org.apache.fineract.investor.data.ExternalTransferStatus; +import org.apache.fineract.investor.data.ExternalTransferSubStatus; +import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer; +import org.apache.fineract.investor.domain.LoanOwnershipTransferBusinessEvent; +import org.apache.fineract.investor.service.ExternalAssetOwnersReadService; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.portfolio.charge.domain.Charge; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +public class InvestorBusinessEventSerializerTest { + + public static final long LOAN_ID = 222L; + public static final String ASSET_OWNER_EXTERNAL_ID = "1ad87015-8b05-49de-9ed8-e9c214fda7eb"; + public static final String LOAN_EXTERNAL_ID = "29641fe0-0ac6-409a-bb8b-24fdf08d2891"; + public static final String TRANSFER_EXTERNAL_ID = "ac303982-46a5-4cea-9f71-67b69063a2b7"; + + @Test + public void testSerializationSellOK() { + doTest(ACTIVE, null, "SALE", "EXECUTED", null); + } + + @Test + public void testSerializationBuybackOK() { + doTest(BUYBACK, null, "BUYBACK", "EXECUTED", null); + } + + @Test + public void testSerializationDeclinedNegativeBalance() { + doTest(DECLINED, BALANCE_NEGATIVE, "SALE", "DECLINED", "BALANCE NEGATIV"); + } + + @Test + public void testSerializationDeclinedBalanceZero() { + doTest(DECLINED, BALANCE_ZERO, "SALE", "DECLINED", "BALANCE ZERO"); + } + + @Test + public void testSerializationCancelledSameDayTransfer() { + doTest(CANCELLED, SAMEDAY_TRANSFERS, "SALE", "CANCELLED", "SAMEDAY TRANSFER"); + } + + private void doTest(ExternalTransferStatus status, ExternalTransferSubStatus subStatus, String expectedType, String expectedStatus, + String expectedReason) { + // given + ExternalAssetOwnersReadService mockReadService = Mockito.mock(ExternalAssetOwnersReadService.class); + when(mockReadService.retrieveTransferData(123L)).thenReturn(createTransferData(status)); + Loan loan = Mockito.mock(Loan.class); + when(loan.getCurrency()).thenReturn(new MonetaryCurrency("EUR", 2, 1)); + List loanCharges = createMockCharges(); + when(loan.getLoanCharges()).thenReturn(loanCharges); + LoanOwnershipTransferBusinessEvent loanOwnershipTransferBusinessEvent = new LoanOwnershipTransferBusinessEvent( + createExternalAssetOwnerTransfer(status, subStatus), loan); + + // when + InvestorBusinessEventSerializer serializer = new InvestorBusinessEventSerializer(mockReadService); + ByteBufferSerializable byteBufferSerializable = serializer.toAvroDTO(loanOwnershipTransferBusinessEvent); + + // then + verifyFields(byteBufferSerializable, expectedType, expectedStatus, expectedReason); + } + + private List createMockCharges() { + List loanCharges = new ArrayList<>(); + loanCharges.add(loanCharge(1L, "charge a", new BigDecimal("10.00000"))); + loanCharges.add(loanCharge(1L, "charge a", new BigDecimal("15.00000"))); + loanCharges.add(loanCharge(2L, "charge b", BigDecimal.ZERO)); + loanCharges.add(loanCharge(3L, "charge c", new BigDecimal("12.00000"))); + return loanCharges; + } + + private LoanCharge loanCharge(Long chargeId, String name, BigDecimal amountOutstanding) { + LoanCharge loanCharge = mock(LoanCharge.class); + Charge charge = mock(Charge.class); + when(charge.getId()).thenReturn(chargeId); + when(charge.getName()).thenReturn(name); + when(loanCharge.name()).thenReturn(name); + when(loanCharge.getCharge()).thenReturn(charge); + when(loanCharge.amountOutstanding()).thenReturn(amountOutstanding); + return loanCharge; + } + + private static void verifyFields(ByteBufferSerializable byteBufferSerializable, String type, String status, String statusReason) { + assertTrue(byteBufferSerializable instanceof LoanOwnershipTransferDataV1); + LoanOwnershipTransferDataV1 result = (LoanOwnershipTransferDataV1) byteBufferSerializable; + assertEquals(LOAN_ID, result.getLoanId()); + assertEquals("EUR", result.getCurrency().getCode()); + assertEquals(2, result.getCurrency().getDecimalPlaces()); + assertEquals(1, result.getCurrency().getInMultiplesOf()); + assertEquals("1.0", result.getPurchasePriceRatio()); + assertEquals(ASSET_OWNER_EXTERNAL_ID, result.getAssetOwnerExternalId()); + assertEquals(LOAN_EXTERNAL_ID, result.getLoanExternalId()); + assertEquals(TRANSFER_EXTERNAL_ID, result.getTransferExternalId()); + assertEquals(LOAN_ID, result.getLoanId()); + assertEquals(new BigDecimal("1108.00000"), result.getTotalOutstandingBalanceAmount()); + assertEquals(new BigDecimal("100.00000"), result.getOutstandingInterestPortion()); + assertEquals(new BigDecimal("1000.00000"), result.getOutstandingPrincipalPortion()); + assertEquals(new BigDecimal("5.00000"), result.getOutstandingFeePortion()); + assertEquals(new BigDecimal("3.00000"), result.getOutstandingPenaltyPortion()); + assertEquals(BigDecimal.ZERO, result.getOverPaymentPortion()); + assertEquals("2023-06-11", result.getSettlementDate()); + assertEquals("2023-06-11", result.getSubmittedDate()); + assertEquals(status, result.getTransferStatus()); + assertEquals(statusReason, result.getTransferStatusReason()); + assertEquals(type, result.getType()); + verifyUnpaidCharges(result.getUnpaidChargeData()); + } + + private static void verifyUnpaidCharges(List unpaidChargeData) { + assertEquals(2, unpaidChargeData.size()); + Map map = unpaidChargeData.stream() + .collect(Collectors.toMap(UnpaidChargeDataV1::getChargeId, Function.identity())); + assertEquals("charge a", map.get(1L).getChargeName()); + assertEquals(new BigDecimal("25.00000"), map.get(1L).getOutstandingAmount()); + assertEquals("charge c", map.get(3L).getChargeName()); + assertEquals(new BigDecimal("12.00000"), map.get(3L).getOutstandingAmount()); + } + + private ExternalAssetOwnerTransfer createExternalAssetOwnerTransfer(ExternalTransferStatus status, + ExternalTransferSubStatus subStatus) { + ExternalAssetOwnerTransfer mock = Mockito.mock(ExternalAssetOwnerTransfer.class); + when(mock.getStatus()).thenReturn(status); + when(mock.getSubStatus()).thenReturn(subStatus); + when(mock.getId()).thenReturn(123L); + return mock; + } + + private ExternalTransferData createTransferData(ExternalTransferStatus status) { + ExternalTransferDataDetails details = new ExternalTransferDataDetails(); + details.setDetailsId(444L); + details.setTotalOutstanding(new BigDecimal("1108.00000")); + details.setTotalInterestOutstanding(new BigDecimal("100.00000")); + details.setTotalPrincipalOutstanding(new BigDecimal("1000.00000")); + details.setTotalFeeChargesOutstanding(new BigDecimal("5.00000")); + details.setTotalPenaltyChargesOutstanding(new BigDecimal("3.00000")); + details.setTotalOverpaid(BigDecimal.ZERO); + + ExternalTransferData data = new ExternalTransferData(); + data.setOwner(new ExternalTransferOwnerData(ASSET_OWNER_EXTERNAL_ID)); + data.setStatus(status); + data.setTransferId(123L); + data.setLoan(new ExternalTransferLoanData(LOAN_ID, LOAN_EXTERNAL_ID)); + data.setEffectiveFrom(LocalDate.of(2023, 6, 10)); + data.setEffectiveTo(LocalDate.of(9999, 12, 31)); + data.setSettlementDate(LocalDate.of(2023, 6, 11)); + data.setPurchasePriceRatio("1.0"); + data.setTransferExternalId(TRANSFER_EXTERNAL_ID); + data.setDetails(details); + return data; + } + +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/LoanAccountSnapshotBusinessEvent.java b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/LoanAccountSnapshotBusinessEvent.java new file mode 100644 index 000000000..aa77dac82 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/LoanAccountSnapshotBusinessEvent.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.infrastructure.event.business.domain.loan; + +import org.apache.fineract.portfolio.loanaccount.domain.Loan; + +public class LoanAccountSnapshotBusinessEvent extends LoanBusinessEvent { + + private static final String TYPE = "LoanAccountSnapshotBusinessEvent"; + + public LoanAccountSnapshotBusinessEvent(Loan 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/LoanBusinessEvent.java b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/LoanBusinessEvent.java similarity index 100% rename from fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/LoanBusinessEvent.java rename to fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/LoanBusinessEvent.java 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 5c5fbe810..4cceda7c6 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 @@ -96,7 +96,7 @@ public class ExternalEventConfigurationValidationServiceTest { "LoanChargeAdjustmentPostBusinessEvent", "LoanChargeAdjustmentPreBusinessEvent", "LoanDelinquencyRangeChangeBusinessEvent", "LoanAccountsStayedLockedBusinessEvent", "MockBusinessEvent", "LoanChargeOffPreBusinessEvent", "LoanChargeOffPostBusinessEvent", "LoanUndoChargeOffBusinessEvent", "LoanAccrualTransactionCreatedBusinessEvent", - "LoanRescheduledDueAdjustScheduleBusinessEvent"); + "LoanRescheduledDueAdjustScheduleBusinessEvent", "LoanOwnershipTransferBusinessEvent", "LoanAccountSnapshotBusinessEvent"); List tenants = Arrays .asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null)); @@ -174,7 +174,7 @@ public class ExternalEventConfigurationValidationServiceTest { "LoanChargeAdjustmentPostBusinessEvent", "LoanChargeAdjustmentPreBusinessEvent", "LoanDelinquencyRangeChangeBusinessEvent", "LoanAccountsStayedLockedBusinessEvent", "LoanChargeOffPreBusinessEvent", "LoanChargeOffPostBusinessEvent", "LoanUndoChargeOffBusinessEvent", "LoanAccrualTransactionCreatedBusinessEvent", - "LoanRescheduledDueAdjustScheduleBusinessEvent"); + "LoanRescheduledDueAdjustScheduleBusinessEvent", "LoanOwnershipTransferBusinessEvent", "LoanAccountSnapshotBusinessEvent"); List tenants = Arrays .asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null)); diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanAccountDelinquencyRangeEventSerializerTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanAccountDelinquencyRangeEventSerializerTest.java index 008b2d96c..807c7cbbf 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanAccountDelinquencyRangeEventSerializerTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanAccountDelinquencyRangeEventSerializerTest.java @@ -105,7 +105,7 @@ public class LoanAccountDelinquencyRangeEventSerializerTest { // given LoanDelinquencyRangeChangeBusinessEventSerializer serializer = new LoanDelinquencyRangeChangeBusinessEventSerializer( loanReadPlatformService, new LoanDelinquencyRangeDataMapperImpl(), loanChargeReadPlatformService, - delinquencyReadPlatformService, new LoanChargeDataMapperImpl(), new CurrencyDataMapperImpl(), mapper); + delinquencyReadPlatformService, new LoanChargeDataMapperImpl(null, null, null), new CurrencyDataMapperImpl(), mapper); Loan loanForProcessing = Mockito.mock(Loan.class); LoanAccountData loanAccountData = mock(LoanAccountData.class); 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 58f7d32bc..b52743696 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 @@ -470,6 +470,16 @@ public class ExternalEventConfigurationHelper { loanRescheduledDueAdjustScheduleBusinessEvent.put("enabled", false); defaults.add(loanRescheduledDueAdjustScheduleBusinessEvent); + Map loanOwnershipTransferBusinessEvent = new HashMap<>(); + loanOwnershipTransferBusinessEvent.put("type", "LoanOwnershipTransferBusinessEvent"); + loanOwnershipTransferBusinessEvent.put("enabled", false); + defaults.add(loanOwnershipTransferBusinessEvent); + + Map loanAccountSnapshotBusinessEvent = new HashMap<>(); + loanAccountSnapshotBusinessEvent.put("type", "LoanAccountSnapshotBusinessEvent"); + loanAccountSnapshotBusinessEvent.put("enabled", false); + defaults.add(loanAccountSnapshotBusinessEvent); + return defaults; }