FINERACT-1926: Fineract Asset Externalization new asset events

- [x] Add LoanOwnershipTransferBusinessEvent
- [x] Add LoanAccountSnapshotBusinessEvent
- [x] Move packages to the right packages
- [X] Implement LoanOwnershipTransferBusinessEvent serializer
- [x] Call cancel/buyback/sale event
- [x] Unit tests
This commit is contained in:
Peter Bagrij
2023-06-13 15:28:51 +02:00
committed by Arnold Gálovics
parent d37c0e1de2
commit 25d7a9228a
20 changed files with 771 additions and 12 deletions
@@ -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"
]
}
]
}
+4 -1
View File
@@ -28,7 +28,6 @@ dependencies {
) )
// implementation dependencies are directly used (compiled against) in src/main (and src/test) // implementation dependencies are directly used (compiled against) in src/main (and src/test)
//
implementation( implementation(
'org.springframework.boot:spring-boot-starter-web', 'org.springframework.boot:spring-boot-starter-web',
'org.springframework.boot:spring-boot-starter-security', 'org.springframework.boot:spring-boot-starter-security',
@@ -127,4 +126,8 @@ dependencies {
exclude group: 'com.zaxxer', module: 'HikariCP-java7' exclude group: 'com.zaxxer', module: 'HikariCP-java7'
} }
testImplementation ('org.mockito:mockito-inline') testImplementation ('org.mockito:mockito-inline')
implementation('org.apache.avro:avro')
implementation(
project(path: ':fineract-avro-schemas')
)
} }
@@ -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.apache.fineract.infrastructure.event.external.service.serialization.mapper.support.ExternalIdMapper;
import org.mapstruct.Builder; import org.mapstruct.Builder;
import org.mapstruct.InjectionStrategy;
import org.mapstruct.MapperConfig; import org.mapstruct.MapperConfig;
import org.mapstruct.MappingConstants; import org.mapstruct.MappingConstants;
import org.mapstruct.ReportingPolicy; import org.mapstruct.ReportingPolicy;
@MapperConfig(componentModel = MappingConstants.ComponentModel.SPRING, unmappedTargetPolicy = ReportingPolicy.ERROR, builder = @Builder(disableBuilder = true), uses = { @MapperConfig(componentModel = MappingConstants.ComponentModel.SPRING, unmappedTargetPolicy = ReportingPolicy.ERROR, builder = @Builder(disableBuilder = true), uses = {
ExternalIdMapper.class }) ExternalIdMapper.class }, injectionStrategy = InjectionStrategy.CONSTRUCTOR)
public class MapstructMapperConfig {} public class MapstructMapperConfig {}
@@ -19,10 +19,11 @@
package org.apache.fineract.infrastructure.event.external.service.serialization.mapper.support; package org.apache.fineract.infrastructure.event.external.service.serialization.mapper.support;
import org.mapstruct.Builder; import org.mapstruct.Builder;
import org.mapstruct.InjectionStrategy;
import org.mapstruct.MapperConfig; import org.mapstruct.MapperConfig;
import org.mapstruct.MappingConstants; import org.mapstruct.MappingConstants;
import org.mapstruct.ReportingPolicy; import org.mapstruct.ReportingPolicy;
@MapperConfig(componentModel = MappingConstants.ComponentModel.SPRING, unmappedTargetPolicy = ReportingPolicy.ERROR, builder = @Builder(disableBuilder = true), uses = { @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 {} public class AvroMapperConfig {}
@@ -26,6 +26,8 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.cob.loan.LoanCOBBusinessStep; import org.apache.fineract.cob.loan.LoanCOBBusinessStep;
import org.apache.fineract.infrastructure.core.service.DateUtils; 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.config.InvestorModuleIsEnabledCondition;
import org.apache.fineract.investor.data.ExternalTransferStatus; import org.apache.fineract.investor.data.ExternalTransferStatus;
import org.apache.fineract.investor.data.ExternalTransferSubStatus; 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.ExternalAssetOwnerTransferLoanMapping;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMappingRepository; import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMappingRepository;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferRepository; 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.investor.service.AccountingService;
import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Conditional;
@@ -50,6 +53,7 @@ public class LoanAccountOwnerTransferBusinessStep implements LoanCOBBusinessStep
private final ExternalAssetOwnerTransferRepository externalAssetOwnerTransferRepository; private final ExternalAssetOwnerTransferRepository externalAssetOwnerTransferRepository;
private final ExternalAssetOwnerTransferLoanMappingRepository externalAssetOwnerTransferLoanMappingRepository; private final ExternalAssetOwnerTransferLoanMappingRepository externalAssetOwnerTransferLoanMappingRepository;
private final AccountingService accountingService; private final AccountingService accountingService;
private final BusinessEventNotifierService businessEventNotifierService;
@Override @Override
public Loan execute(Loan loan) { 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", throw new IllegalStateException(String.format("Illegal transfer found. Expected %s and %s, found: %s and %s",
ExternalTransferStatus.PENDING, ExternalTransferStatus.BUYBACK, firstTransferStatus, secondTransferStatus)); ExternalTransferStatus.PENDING, ExternalTransferStatus.BUYBACK, firstTransferStatus, secondTransferStatus));
} }
handleSameDaySaleAndBuyback(settlementDate, transferDataList); handleSameDaySaleAndBuyback(settlementDate, transferDataList, loan);
} else if (size == 1) { } else if (size == 1) {
ExternalAssetOwnerTransfer transfer = transferDataList.get(0); ExternalAssetOwnerTransfer transfer = transferDataList.get(0);
if (ExternalTransferStatus.PENDING.equals(transfer.getStatus())) { 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) { private void handleSale(final Loan loan, final LocalDate settlementDate, final ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
ExternalAssetOwnerTransfer newExternalAssetOwnerTransfer = sellAsset(loan, settlementDate, 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, private void handleBuyback(final Loan loan, final LocalDate settlementDate,
@@ -102,7 +107,8 @@ public class LoanAccountOwnerTransferBusinessStep implements LoanCOBBusinessStep
.orElseThrow(); .orElseThrow();
ExternalAssetOwnerTransfer newExternalAssetOwnerTransfer = buybackAsset(loan, settlementDate, buybackExternalAssetOwnerTransfer, ExternalAssetOwnerTransfer newExternalAssetOwnerTransfer = buybackAsset(loan, settlementDate, buybackExternalAssetOwnerTransfer,
activeExternalAssetOwnerTransfer); 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, 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; return loan.getLoanSummary().getTotalOutstanding().compareTo(BigDecimal.ZERO) > 0;
} }
private void handleSameDaySaleAndBuyback(final LocalDate settlementDate, final List<ExternalAssetOwnerTransfer> transferDataList) { private void handleSameDaySaleAndBuyback(final LocalDate settlementDate, final List<ExternalAssetOwnerTransfer> transferDataList,
Loan loan) {
ExternalAssetOwnerTransfer cancelledPendingTransfer = cancelTransfer(settlementDate, transferDataList.get(0)); ExternalAssetOwnerTransfer cancelledPendingTransfer = cancelTransfer(settlementDate, transferDataList.get(0));
ExternalAssetOwnerTransfer cancelledBuybackTransfer = cancelTransfer(settlementDate, transferDataList.get(1)); 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, private ExternalAssetOwnerTransfer cancelTransfer(final LocalDate settlementDate,
@@ -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<ExternalAssetOwnerTransfer> {
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();
}
}
@@ -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;
}
}
@@ -30,4 +30,9 @@ public class ExternalAssetOwnerTransferNotFoundException extends AbstractPlatfor
externalTransferStatus), externalTransferStatus),
externalId.getValue(), 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);
}
} }
@@ -28,6 +28,8 @@ public interface ExternalAssetOwnersReadService {
Page<ExternalTransferData> retrieveTransferData(Long loanId, String externalLoanId, String transferExternalId, Integer offset, Page<ExternalTransferData> retrieveTransferData(Long loanId, String externalLoanId, String transferExternalId, Integer offset,
Integer limit); Integer limit);
ExternalTransferData retrieveTransferData(Long transferExternalId);
ExternalTransferData retrieveActiveTransferData(Long loanId, String externalLoanId, String transferExternalId); ExternalTransferData retrieveActiveTransferData(Long loanId, String externalLoanId, String transferExternalId);
ExternalOwnerTransferJournalEntryData retrieveJournalEntriesOfTransfer(Long transferId, Integer offset, Integer limit); ExternalOwnerTransferJournalEntryData retrieveJournalEntriesOfTransfer(Long transferId, Integer offset, Integer limit);
@@ -33,6 +33,7 @@ import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferJournalEntr
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMapping; import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMapping;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMappingRepository; import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMappingRepository;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferRepository; 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.Page;
import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
@@ -122,4 +123,10 @@ public class ExternalAssetOwnersReadServiceImpl implements ExternalAssetOwnersRe
return PageRequest.of(offset, limit, Sort.by("id")); 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));
}
} }
@@ -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 <T> boolean canSerialize(BusinessEvent<T> event) {
return event instanceof InvestorBusinessEvent;
}
@Override
public Class<? extends GenericContainer> getSupportedSchema() {
return LoanOwnershipTransferDataV1.class;
}
@Override
public <T> ByteBufferSerializable toAvroDTO(BusinessEvent<T> 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<UnpaidChargeDataV1> getUnpaidChargeData(InvestorBusinessEvent event) {
java.util.Map<Long, UnpaidChargeDataV1> map = new HashMap<>();
event.getLoan().getLoanCharges().forEach(loanCharge -> addToMap(map, loanCharge));
return map.values().stream().toList();
}
private void addToMap(Map<Long, UnpaidChargeDataV1> 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<String> 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();
}
}
}
@@ -30,4 +30,5 @@
<include relativeToChangelogFile="true" file="parts/0006_asset_schemas.xml"/> <include relativeToChangelogFile="true" file="parts/0006_asset_schemas.xml"/>
<include relativeToChangelogFile="true" file="parts/0007_add_external_asset_owner_transfer_details.xml"/> <include relativeToChangelogFile="true" file="parts/0007_add_external_asset_owner_transfer_details.xml"/>
<include relativeToChangelogFile="true" file="parts/0008_add_mappings.xml"/> <include relativeToChangelogFile="true" file="parts/0008_add_mappings.xml"/>
<include relativeToChangelogFile="true" file="parts/0009_add_loan_ownership_transfer_events.xml"/>
</databaseChangeLog> </databaseChangeLog>
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet author="fineract" id="1">
<insert tableName="m_external_event_configuration">
<column name="type" value="LoanOwnershipTransferBusinessEvent"/>
<column name="enabled" valueBoolean="false"/>
</insert>
<insert tableName="m_external_event_configuration">
<column name="type" value="LoanAccountSnapshotBusinessEvent"/>
<column name="enabled" valueBoolean="false"/>
</insert>
</changeSet>
</databaseChangeLog>
@@ -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.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows; 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.any;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import java.math.BigDecimal; 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.ActionContext;
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant; import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; 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.ExternalTransferStatus;
import org.apache.fineract.investor.data.ExternalTransferSubStatus; import org.apache.fineract.investor.data.ExternalTransferSubStatus;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer; import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMapping; import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMapping;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMappingRepository; import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMappingRepository;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferRepository; 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.investor.service.AccountingService;
import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanSummary; import org.apache.fineract.portfolio.loanaccount.domain.LoanSummary;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
@@ -66,6 +73,10 @@ public class LoanAccountOwnerTransferBusinessStepTest {
private ExternalAssetOwnerTransferRepository externalAssetOwnerTransferRepository; private ExternalAssetOwnerTransferRepository externalAssetOwnerTransferRepository;
@Mock @Mock
private ExternalAssetOwnerTransferLoanMappingRepository externalAssetOwnerTransferLoanMappingRepository; private ExternalAssetOwnerTransferLoanMappingRepository externalAssetOwnerTransferLoanMappingRepository;
@Mock
private BusinessEventNotifierService businessEventNotifierService;
@Mock @Mock
private AccountingService accountingService; private AccountingService accountingService;
private LoanAccountOwnerTransferBusinessStep underTest; private LoanAccountOwnerTransferBusinessStep underTest;
@@ -76,7 +87,7 @@ public class LoanAccountOwnerTransferBusinessStepTest {
ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT); ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
ThreadLocalContextUtil.setBusinessDates(new HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, actualDate))); ThreadLocalContextUtil.setBusinessDates(new HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, actualDate)));
underTest = new LoanAccountOwnerTransferBusinessStep(externalAssetOwnerTransferRepository, underTest = new LoanAccountOwnerTransferBusinessStep(externalAssetOwnerTransferRepository,
externalAssetOwnerTransferLoanMappingRepository, accountingService); externalAssetOwnerTransferLoanMappingRepository, accountingService, businessEventNotifierService);
} }
@Test @Test
@@ -89,6 +100,7 @@ public class LoanAccountOwnerTransferBusinessStepTest {
final Loan processedLoan = underTest.execute(loanForProcessing); final Loan processedLoan = underTest.execute(loanForProcessing);
// then // then
verify(externalAssetOwnerTransferRepository, times(1)).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id"))); verify(externalAssetOwnerTransferRepository, times(1)).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")));
verifyNoInteractions(businessEventNotifierService);
assertEquals(processedLoan, loanForProcessing); assertEquals(processedLoan, loanForProcessing);
} }
@@ -109,6 +121,7 @@ public class LoanAccountOwnerTransferBusinessStepTest {
// then // then
assertEquals("Illegal transfer found. Expected PENDING and BUYBACK, found: PENDING and ACTIVE", exception.getMessage()); 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"))); verify(externalAssetOwnerTransferRepository, times(1)).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")));
verifyNoInteractions(businessEventNotifierService);
} }
@Test @Test
@@ -118,6 +131,15 @@ public class LoanAccountOwnerTransferBusinessStepTest {
when(loanForProcessing.getId()).thenReturn(1L); when(loanForProcessing.getId()).thenReturn(1L);
ExternalAssetOwnerTransfer firstResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class); ExternalAssetOwnerTransfer firstResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
ExternalAssetOwnerTransfer secondResponseItem = 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(firstResponseItem.getStatus()).thenReturn(ExternalTransferStatus.PENDING);
when(secondResponseItem.getStatus()).thenReturn(ExternalTransferStatus.BUYBACK); when(secondResponseItem.getStatus()).thenReturn(ExternalTransferStatus.BUYBACK);
List<ExternalAssetOwnerTransfer> response = List.of(firstResponseItem, secondResponseItem); List<ExternalAssetOwnerTransfer> response = List.of(firstResponseItem, secondResponseItem);
@@ -163,6 +185,11 @@ public class LoanAccountOwnerTransferBusinessStepTest {
assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(3).getEffectiveDateTo()); assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(3).getEffectiveDateTo());
assertEquals(processedLoan, loanForProcessing); assertEquals(processedLoan, loanForProcessing);
ArgumentCaptor<BusinessEvent<?>> businessEventArgumentCaptor = verifyBusinessEvents(3);
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, secondSaveResult);
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 1, loanForProcessing, fourthSaveResult);
verifyLoanAccountSnapshotBusinessEvent(businessEventArgumentCaptor, 2, loanForProcessing);
} }
@Test @Test
@@ -193,6 +220,10 @@ public class LoanAccountOwnerTransferBusinessStepTest {
verify(externalAssetOwnerTransferLoanMappingRepository, times(1)).deleteByLoanIdAndOwnerTransfer(1L, secondResponseItem); verify(externalAssetOwnerTransferLoanMappingRepository, times(1)).deleteByLoanIdAndOwnerTransfer(1L, secondResponseItem);
assertEquals(processedLoan, loanForProcessing); assertEquals(processedLoan, loanForProcessing);
ArgumentCaptor<BusinessEvent<?>> businessEventArgumentCaptor = verifyBusinessEvents(2);
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, firstResponseItem);
verifyLoanAccountSnapshotBusinessEvent(businessEventArgumentCaptor, 1, loanForProcessing);
} }
@Test @Test
@@ -238,6 +269,10 @@ public class LoanAccountOwnerTransferBusinessStepTest {
assertEquals(1L, externalAssetOwnerTransferLoanMappingArgumentCaptor.getValue().getLoanId()); assertEquals(1L, externalAssetOwnerTransferLoanMappingArgumentCaptor.getValue().getLoanId());
assertEquals(newTransfer, externalAssetOwnerTransferLoanMappingArgumentCaptor.getValue().getOwnerTransfer()); assertEquals(newTransfer, externalAssetOwnerTransferLoanMappingArgumentCaptor.getValue().getOwnerTransfer());
assertEquals(processedLoan, loanForProcessing); assertEquals(processedLoan, loanForProcessing);
ArgumentCaptor<BusinessEvent<?>> businessEventArgumentCaptor = verifyBusinessEvents(2);
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, newTransfer);
verifyLoanAccountSnapshotBusinessEvent(businessEventArgumentCaptor, 1, loanForProcessing);
} }
@Test @Test
@@ -278,6 +313,10 @@ public class LoanAccountOwnerTransferBusinessStepTest {
assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getEffectiveDateFrom()); assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getEffectiveDateFrom());
assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getEffectiveDateTo()); assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getEffectiveDateTo());
assertEquals(processedLoan, loanForProcessing); assertEquals(processedLoan, loanForProcessing);
ArgumentCaptor<BusinessEvent<?>> businessEventArgumentCaptor = verifyBusinessEvents(2);
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, newTransfer);
verifyLoanAccountSnapshotBusinessEvent(businessEventArgumentCaptor, 1, loanForProcessing);
} }
@Test @Test
@@ -318,6 +357,10 @@ public class LoanAccountOwnerTransferBusinessStepTest {
assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getEffectiveDateFrom()); assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getEffectiveDateFrom());
assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getEffectiveDateTo()); assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getEffectiveDateTo());
assertEquals(processedLoan, loanForProcessing); assertEquals(processedLoan, loanForProcessing);
ArgumentCaptor<BusinessEvent<?>> businessEventArgumentCaptor = verifyBusinessEvents(2);
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, newTransfer);
verifyLoanAccountSnapshotBusinessEvent(businessEventArgumentCaptor, 1, loanForProcessing);
} }
@Test @Test
@@ -333,4 +376,26 @@ public class LoanAccountOwnerTransferBusinessStepTest {
assertNotNull(actualEnumName); assertNotNull(actualEnumName);
assertEquals("Execute external asset owner transfer", actualEnumName); assertEquals("Execute external asset owner transfer", actualEnumName);
} }
@NotNull
private ArgumentCaptor<BusinessEvent<?>> verifyBusinessEvents(int expectedBusinessEvents) {
@SuppressWarnings("unchecked")
ArgumentCaptor<BusinessEvent<?>> businessEventArgumentCaptor = ArgumentCaptor.forClass(BusinessEvent.class);
verify(businessEventNotifierService, times(expectedBusinessEvents)).notifyPostBusinessEvent(businessEventArgumentCaptor.capture());
return businessEventArgumentCaptor;
}
private void verifyLoanTransferBusinessEvent(ArgumentCaptor<BusinessEvent<?>> 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<BusinessEvent<?>> businessEventArgumentCaptor, int index,
Loan expectedLoan) {
assertTrue(businessEventArgumentCaptor.getAllValues().get(index) instanceof LoanAccountSnapshotBusinessEvent);
assertEquals(expectedLoan, ((LoanAccountSnapshotBusinessEvent) businessEventArgumentCaptor.getAllValues().get(index)).get());
}
} }
@@ -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<LoanCharge> 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<LoanCharge> createMockCharges() {
List<LoanCharge> 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<UnpaidChargeDataV1> unpaidChargeData) {
assertEquals(2, unpaidChargeData.size());
Map<Long, UnpaidChargeDataV1> 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;
}
}
@@ -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;
}
}
@@ -96,7 +96,7 @@ public class ExternalEventConfigurationValidationServiceTest {
"LoanChargeAdjustmentPostBusinessEvent", "LoanChargeAdjustmentPreBusinessEvent", "LoanDelinquencyRangeChangeBusinessEvent", "LoanChargeAdjustmentPostBusinessEvent", "LoanChargeAdjustmentPreBusinessEvent", "LoanDelinquencyRangeChangeBusinessEvent",
"LoanAccountsStayedLockedBusinessEvent", "MockBusinessEvent", "LoanChargeOffPreBusinessEvent", "LoanAccountsStayedLockedBusinessEvent", "MockBusinessEvent", "LoanChargeOffPreBusinessEvent",
"LoanChargeOffPostBusinessEvent", "LoanUndoChargeOffBusinessEvent", "LoanAccrualTransactionCreatedBusinessEvent", "LoanChargeOffPostBusinessEvent", "LoanUndoChargeOffBusinessEvent", "LoanAccrualTransactionCreatedBusinessEvent",
"LoanRescheduledDueAdjustScheduleBusinessEvent"); "LoanRescheduledDueAdjustScheduleBusinessEvent", "LoanOwnershipTransferBusinessEvent", "LoanAccountSnapshotBusinessEvent");
List<FineractPlatformTenant> tenants = Arrays List<FineractPlatformTenant> tenants = Arrays
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null)); .asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
@@ -174,7 +174,7 @@ public class ExternalEventConfigurationValidationServiceTest {
"LoanChargeAdjustmentPostBusinessEvent", "LoanChargeAdjustmentPreBusinessEvent", "LoanDelinquencyRangeChangeBusinessEvent", "LoanChargeAdjustmentPostBusinessEvent", "LoanChargeAdjustmentPreBusinessEvent", "LoanDelinquencyRangeChangeBusinessEvent",
"LoanAccountsStayedLockedBusinessEvent", "LoanChargeOffPreBusinessEvent", "LoanChargeOffPostBusinessEvent", "LoanAccountsStayedLockedBusinessEvent", "LoanChargeOffPreBusinessEvent", "LoanChargeOffPostBusinessEvent",
"LoanUndoChargeOffBusinessEvent", "LoanAccrualTransactionCreatedBusinessEvent", "LoanUndoChargeOffBusinessEvent", "LoanAccrualTransactionCreatedBusinessEvent",
"LoanRescheduledDueAdjustScheduleBusinessEvent"); "LoanRescheduledDueAdjustScheduleBusinessEvent", "LoanOwnershipTransferBusinessEvent", "LoanAccountSnapshotBusinessEvent");
List<FineractPlatformTenant> tenants = Arrays List<FineractPlatformTenant> tenants = Arrays
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null)); .asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
@@ -105,7 +105,7 @@ public class LoanAccountDelinquencyRangeEventSerializerTest {
// given // given
LoanDelinquencyRangeChangeBusinessEventSerializer serializer = new LoanDelinquencyRangeChangeBusinessEventSerializer( LoanDelinquencyRangeChangeBusinessEventSerializer serializer = new LoanDelinquencyRangeChangeBusinessEventSerializer(
loanReadPlatformService, new LoanDelinquencyRangeDataMapperImpl(), loanChargeReadPlatformService, 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); Loan loanForProcessing = Mockito.mock(Loan.class);
LoanAccountData loanAccountData = mock(LoanAccountData.class); LoanAccountData loanAccountData = mock(LoanAccountData.class);
@@ -470,6 +470,16 @@ public class ExternalEventConfigurationHelper {
loanRescheduledDueAdjustScheduleBusinessEvent.put("enabled", false); loanRescheduledDueAdjustScheduleBusinessEvent.put("enabled", false);
defaults.add(loanRescheduledDueAdjustScheduleBusinessEvent); defaults.add(loanRescheduledDueAdjustScheduleBusinessEvent);
Map<String, Object> loanOwnershipTransferBusinessEvent = new HashMap<>();
loanOwnershipTransferBusinessEvent.put("type", "LoanOwnershipTransferBusinessEvent");
loanOwnershipTransferBusinessEvent.put("enabled", false);
defaults.add(loanOwnershipTransferBusinessEvent);
Map<String, Object> loanAccountSnapshotBusinessEvent = new HashMap<>();
loanAccountSnapshotBusinessEvent.put("type", "LoanAccountSnapshotBusinessEvent");
loanAccountSnapshotBusinessEvent.put("enabled", false);
defaults.add(loanAccountSnapshotBusinessEvent);
return defaults; return defaults;
} }