You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fineract.apache.org by ar...@apache.org on 2023/06/13 13:29:00 UTC

[fineract] branch develop updated: 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 is an automated email from the ASF dual-hosted git repository.

arnold pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git


The following commit(s) were added to refs/heads/develop by this push:
     new 25d7a9228 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
25d7a9228 is described below

commit 25d7a9228a930c58a7c9612ae03d6c21abc303c7
Author: Peter Bagrij <pe...@dpc.hu>
AuthorDate: Tue Jun 13 13:52:09 2023 +0200

    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
---
 .../avro/loan/v1/LoanOwnershipTransferDataV1.avsc  | 158 ++++++++++++++++
 fineract-core/dependencies.gradle                  |   5 +-
 .../core/config/MapstructMapperConfig.java         |   3 +-
 .../mapper/support/AvroMapperConfig.java           |   3 +-
 .../loan/LoanAccountOwnerTransferBusinessStep.java |  19 +-
 .../investor/domain/InvestorBusinessEvent.java     |  12 +-
 .../domain/LoanOwnershipTransferBusinessEvent.java |  20 +--
 ...xternalAssetOwnerTransferNotFoundException.java |   5 +
 .../service/ExternalAssetOwnersReadService.java    |   2 +
 .../ExternalAssetOwnersReadServiceImpl.java        |   7 +
 .../investor/InvestorBusinessEventSerializer.java  | 145 +++++++++++++++
 .../module/investor/module-changelog-master.xml    |   1 +
 .../0009_add_loan_ownership_transfer_events.xml}   |  24 +--
 .../LoanAccountOwnerTransferBusinessStepTest.java  |  67 ++++++-
 .../InvestorBusinessEventSerializerTest.java       | 200 +++++++++++++++++++++
 .../loan/LoanAccountSnapshotBusinessEvent.java     |  16 +-
 .../business/domain/loan/LoanBusinessEvent.java    |   0
 ...nalEventConfigurationValidationServiceTest.java |   4 +-
 ...AccountDelinquencyRangeEventSerializerTest.java |   2 +-
 .../common/ExternalEventConfigurationHelper.java   |  10 ++
 20 files changed, 653 insertions(+), 50 deletions(-)

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<ExternalAssetOwnerTransfer> transferDataList) {
+    private void handleSameDaySaleAndBuyback(final LocalDate settlementDate, final List<ExternalAssetOwnerTransfer> 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-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/LoanBusinessEvent.java b/fineract-investor/src/main/java/org/apache/fineract/investor/domain/InvestorBusinessEvent.java
similarity index 75%
copy from fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/LoanBusinessEvent.java
copy to fineract-investor/src/main/java/org/apache/fineract/investor/domain/InvestorBusinessEvent.java
index 62efeab55..76ccb5b2e 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/LoanBusinessEvent.java
+++ b/fineract-investor/src/main/java/org/apache/fineract/investor/domain/InvestorBusinessEvent.java
@@ -16,17 +16,21 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.fineract.infrastructure.event.business.domain.loan;
+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;
 
-public abstract class LoanBusinessEvent extends AbstractBusinessEvent<Loan> {
+@Getter
+public abstract class InvestorBusinessEvent extends AbstractBusinessEvent<ExternalAssetOwnerTransfer> {
 
-    private static final String CATEGORY = "Loan";
+    private final Loan loan;
+    private static final String CATEGORY = "Investor";
 
-    public LoanBusinessEvent(Loan value) {
+    public InvestorBusinessEvent(ExternalAssetOwnerTransfer value, Loan loan) {
         super(value);
+        this.loan = loan;
     }
 
     @Override
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/LoanBusinessEvent.java b/fineract-investor/src/main/java/org/apache/fineract/investor/domain/LoanOwnershipTransferBusinessEvent.java
similarity index 64%
copy from fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/LoanBusinessEvent.java
copy to fineract-investor/src/main/java/org/apache/fineract/investor/domain/LoanOwnershipTransferBusinessEvent.java
index 62efeab55..b6aa67068 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/LoanBusinessEvent.java
+++ b/fineract-investor/src/main/java/org/apache/fineract/investor/domain/LoanOwnershipTransferBusinessEvent.java
@@ -16,26 +16,20 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.fineract.infrastructure.event.business.domain.loan;
+package org.apache.fineract.investor.domain;
 
-import org.apache.fineract.infrastructure.event.business.domain.AbstractBusinessEvent;
 import org.apache.fineract.portfolio.loanaccount.domain.Loan;
 
-public abstract class LoanBusinessEvent extends AbstractBusinessEvent<Loan> {
+public class LoanOwnershipTransferBusinessEvent extends InvestorBusinessEvent {
 
-    private static final String CATEGORY = "Loan";
+    private static final String TYPE = "LoanOwnershipTransferBusinessEvent";
 
-    public LoanBusinessEvent(Loan value) {
-        super(value);
+    public LoanOwnershipTransferBusinessEvent(ExternalAssetOwnerTransfer value, Loan loan) {
+        super(value, loan);
     }
 
     @Override
-    public String getCategory() {
-        return CATEGORY;
-    }
-
-    @Override
-    public Long getAggregateRootId() {
-        return get().getId();
+    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<ExternalTransferData> 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 <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();
+        }
+    }
+}
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 @@
   <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/0008_add_mappings.xml"/>
+  <include relativeToChangelogFile="true" file="parts/0009_add_loan_ownership_transfer_events.xml"/>
 </databaseChangeLog>
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/parts/0009_add_loan_ownership_transfer_events.xml
similarity index 51%
copy from fineract-investor/src/main/resources/db/changelog/tenant/module/investor/module-changelog-master.xml
copy to fineract-investor/src/main/resources/db/changelog/tenant/module/investor/parts/0009_add_loan_ownership_transfer_events.xml
index a6cb411e3..6395fc13b 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/parts/0009_add_loan_ownership_transfer_events.xml
@@ -20,14 +20,18 @@
 
 -->
 <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">
-  <include relativeToChangelogFile="true" file="parts/0001_initial_schema.xml"/>
-  <include relativeToChangelogFile="true" file="parts/0002_asset_schemas.xml"/>
-  <include relativeToChangelogFile="true" file="parts/0003_asset_schemas.xml"/>
-  <include relativeToChangelogFile="true" file="parts/0004_change_purchase_price_ratio_type.xml"/>
-  <include relativeToChangelogFile="true" file="parts/0005_add_sale_and_buyback_command.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/0008_add_mappings.xml"/>
+                   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>
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<ExternalAssetOwnerTransfer> response = List.of(firstResponseItem, secondResponseItem);
@@ -163,6 +185,11 @@ public class LoanAccountOwnerTransferBusinessStepTest {
         assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(3).getEffectiveDateTo());
 
         assertEquals(processedLoan, loanForProcessing);
+
+        ArgumentCaptor<BusinessEvent<?>> 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<BusinessEvent<?>> 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<BusinessEvent<?>> 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<BusinessEvent<?>> 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<BusinessEvent<?>> 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<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());
+    }
 }
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<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;
+    }
+
+}
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/LoanAccountSnapshotBusinessEvent.java
similarity index 70%
copy from fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/LoanBusinessEvent.java
copy to fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/LoanAccountSnapshotBusinessEvent.java
index 62efeab55..aa77dac82 100644
--- 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/LoanAccountSnapshotBusinessEvent.java
@@ -18,24 +18,18 @@
  */
 package org.apache.fineract.infrastructure.event.business.domain.loan;
 
-import org.apache.fineract.infrastructure.event.business.domain.AbstractBusinessEvent;
 import org.apache.fineract.portfolio.loanaccount.domain.Loan;
 
-public abstract class LoanBusinessEvent extends AbstractBusinessEvent<Loan> {
+public class LoanAccountSnapshotBusinessEvent extends LoanBusinessEvent {
 
-    private static final String CATEGORY = "Loan";
+    private static final String TYPE = "LoanAccountSnapshotBusinessEvent";
 
-    public LoanBusinessEvent(Loan value) {
+    public LoanAccountSnapshotBusinessEvent(Loan value) {
         super(value);
     }
 
     @Override
-    public String getCategory() {
-        return CATEGORY;
-    }
-
-    @Override
-    public Long getAggregateRootId() {
-        return get().getId();
+    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<FineractPlatformTenant> 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<FineractPlatformTenant> 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<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;
 
     }