diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
index 231310cf6..34da1913f 100644
--- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
+++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
@@ -227,4 +227,5 @@
+
diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0209_transaction_summary_with_asset_owner_and_from_asset_owner_id_for_asset_sales.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0209_transaction_summary_with_asset_owner_and_from_asset_owner_id_for_asset_sales.xml
new file mode 100644
index 000000000..db2db12d2
--- /dev/null
+++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0209_transaction_summary_with_asset_owner_and_from_asset_owner_id_for_asset_sales.xml
@@ -0,0 +1,3120 @@
+
+
+
+
+
+
+ report_name='Transaction Summary Report with Asset Owner'
+
+
+
+
+
+ report_name='Transaction Summary Report with Asset Owner'
+
+
+
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/InitiateExternalAssetOwnerTransferTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/InitiateExternalAssetOwnerTransferTest.java
index 54a000985..7262613a9 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/InitiateExternalAssetOwnerTransferTest.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/InitiateExternalAssetOwnerTransferTest.java
@@ -53,6 +53,7 @@ import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
+import okhttp3.ResponseBody;
import org.apache.fineract.accounting.common.AccountingConstants;
import org.apache.fineract.accounting.journalentry.domain.JournalEntryType;
import org.apache.fineract.client.models.ExternalAssetOwnerRequest;
@@ -105,6 +106,7 @@ import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.lang.NonNull;
+import retrofit2.Response;
@SuppressWarnings("rawtypes")
@ExtendWith({ ExternalEventsExtension.class })
@@ -1080,8 +1082,14 @@ public class InitiateExternalAssetOwnerTransferTest extends BaseLoanIntegrationT
assertNotNull(jsonPath.getString("columnHeaders[9].columnDisplayType"));
assertFalse(jsonPath.getBoolean("columnHeaders[9].isColumnNullable"));
+ assertNotNull(jsonPath.getString("columnHeaders[10].columnType"));
+ assertNotNull(jsonPath.getString("columnHeaders[10].columnDisplayType"));
+ assertFalse(jsonPath.getBoolean("columnHeaders[10].isColumnNullable"));
+
assertNotNull(retrieveResponse.getContent().get(0).getOwner());
final var ownerId = retrieveResponse.getContent().get(0).getOwner().getExternalId();
+ assertNotNull(retrieveResponse.getContent().get(2).getPreviousOwner());
+ final var previousOwnerId = retrieveResponse.getContent().get(2).getPreviousOwner().getExternalId();
assertEquals("2020-03-03", jsonPath.getString("data[0].row[0]"));
assertTrue(jsonPath.getString("data[0].row[1]").matches("^LOAN_PRODUCT_.{6}$"));
@@ -1093,6 +1101,7 @@ public class InitiateExternalAssetOwnerTransferTest extends BaseLoanIntegrationT
assertNull(jsonPath.getString("data[0].row[7]"));
assertEquals(9.68, jsonPath.getDouble("data[0].row[8]"), 0.01);
assertEquals(ownerId, jsonPath.getString("data[0].row[9]"));
+ assertNull(jsonPath.getString("data[0].row[10]"));
assertEquals("2020-03-03", jsonPath.getString("data[1].row[0]"));
assertTrue(jsonPath.getString("data[1].row[1]").matches("^LOAN_PRODUCT_.{6}$"));
@@ -1104,6 +1113,7 @@ public class InitiateExternalAssetOwnerTransferTest extends BaseLoanIntegrationT
assertNull(jsonPath.getString("data[1].row[7]"));
assertEquals(-757.42, jsonPath.getDouble("data[1].row[8]"), 0.01);
assertNull(jsonPath.getString("data[1].row[9]"));
+ assertEquals(previousOwnerId, jsonPath.getString("data[1].row[10]"));
assertEquals("2020-03-03", jsonPath.getString("data[2].row[0]"));
assertTrue(jsonPath.getString("data[2].row[1]").matches("^LOAN_PRODUCT_.{6}$"));
@@ -1115,6 +1125,7 @@ public class InitiateExternalAssetOwnerTransferTest extends BaseLoanIntegrationT
assertNull(jsonPath.getString("data[2].row[7]"));
assertEquals(-5.00, jsonPath.getDouble("data[2].row[8]"), 0.01);
assertNull(jsonPath.getString("data[2].row[9]"));
+ assertEquals(previousOwnerId, jsonPath.getString("data[2].row[10]"));
assertEquals("2020-03-03", jsonPath.getString("data[3].row[0]"));
assertTrue(jsonPath.getString("data[3].row[1]").matches("^LOAN_PRODUCT_.{6}$"));
@@ -1126,6 +1137,7 @@ public class InitiateExternalAssetOwnerTransferTest extends BaseLoanIntegrationT
assertNull(jsonPath.getString("data[3].row[7]"));
assertEquals(-15000.00, jsonPath.getDouble("data[3].row[8]"), 0.01);
assertNull(jsonPath.getString("data[3].row[9]"));
+ assertEquals(previousOwnerId, jsonPath.getString("data[3].row[10]"));
assertEquals("2020-03-03", jsonPath.getString("data[4].row[0]"));
assertTrue(jsonPath.getString("data[4].row[1]").matches("^LOAN_PRODUCT_.{6}$"));
@@ -1137,6 +1149,7 @@ public class InitiateExternalAssetOwnerTransferTest extends BaseLoanIntegrationT
assertNull(jsonPath.getString("data[4].row[7]"));
assertEquals(0.00, jsonPath.getDouble("data[4].row[8]"), 0.01);
assertEquals(ownerId, jsonPath.getString("data[4].row[9]"));
+ assertNull(jsonPath.getString("data[4].row[10]"));
assertEquals("2020-03-03", jsonPath.getString("data[5].row[0]"));
assertTrue(jsonPath.getString("data[5].row[1]").matches("^LOAN_PRODUCT_.{6}$"));
@@ -1148,6 +1161,7 @@ public class InitiateExternalAssetOwnerTransferTest extends BaseLoanIntegrationT
assertNull(jsonPath.getString("data[5].row[7]"));
assertEquals(0.00, jsonPath.getDouble("data[5].row[8]"), 0.01);
assertEquals(ownerId, jsonPath.getString("data[5].row[9]"));
+ assertNull(jsonPath.getString("data[5].row[10]"));
assertEquals("2020-03-03", jsonPath.getString("data[6].row[0]"));
assertTrue(jsonPath.getString("data[6].row[1]").matches("^LOAN_PRODUCT_.{6}$"));
@@ -1159,6 +1173,7 @@ public class InitiateExternalAssetOwnerTransferTest extends BaseLoanIntegrationT
assertNull(jsonPath.getString("data[6].row[7]"));
assertEquals(-5.00, jsonPath.getDouble("data[6].row[8]"), 0.01);
assertEquals(ownerId, jsonPath.getString("data[6].row[9]"));
+ assertNull(jsonPath.getString("data[6].row[10]"));
assertEquals("2020-03-03", jsonPath.getString("data[7].row[0]"));
assertTrue(jsonPath.getString("data[7].row[1]").matches("^LOAN_PRODUCT_.{6}$"));
@@ -1170,6 +1185,7 @@ public class InitiateExternalAssetOwnerTransferTest extends BaseLoanIntegrationT
assertNull(jsonPath.getString("data[7].row[7]"));
assertEquals(0.00, jsonPath.getDouble("data[7].row[8]"), 0.01);
assertEquals(ownerId, jsonPath.getString("data[7].row[9]"));
+ assertNull(jsonPath.getString("data[7].row[10]"));
assertEquals("2020-03-03", jsonPath.getString("data[8].row[0]"));
assertTrue(jsonPath.getString("data[8].row[1]").matches("^LOAN_PRODUCT_.{6}$"));
@@ -1181,6 +1197,101 @@ public class InitiateExternalAssetOwnerTransferTest extends BaseLoanIntegrationT
assertNull(jsonPath.getString("data[8].row[7]"));
assertEquals(0.00, jsonPath.getDouble("data[8].row[8]"), 0.01);
assertEquals(ownerId, jsonPath.getString("data[8].row[9]"));
+ assertNull(jsonPath.getString("data[8].row[10]"));
+ } finally {
+ ExternalEventHelper.deleteAllExternalEvents(REQUEST_SPEC, new ResponseSpecBuilder().expectStatusCode(Matchers.is(204)).build());
+ cleanUpAndRestoreBusinessDate();
+ }
+ }
+
+ @Test
+ public void transactionSummaryReportWithAssetOwner_CheckFromAssetOwnerIdForBuyback() throws IOException {
+ try {
+ globalConfigurationHelper.manageConfigurations(GlobalConfigurationConstants.ENABLE_AUTO_GENERATED_EXTERNAL_ID, true);
+ setInitialBusinessDate("2023-08-16");
+
+ ExternalEventHelper.deleteAllExternalEvents(REQUEST_SPEC, new ResponseSpecBuilder().expectStatusCode(Matchers.is(204)).build());
+ ExternalEventHelper.changeEventState(REQUEST_SPEC, RESPONSE_SPEC, "LoanOwnershipTransferBusinessEvent", true);
+
+ final Integer officeId = OFFICE_HELPER.createOffice("1 January 2020");
+ final Integer clientID = ClientHelper.createClient(REQUEST_SPEC, RESPONSE_SPEC, "1 January 2020", officeId.toString());
+ final Integer loanID = createLoanForClient(clientID);
+
+ // Create first sale transfer
+ final PostInitiateTransferResponse firstSaleTransferResponse = createSaleTransfer(loanID, "2023-08-16");
+ validateResponse(firstSaleTransferResponse, loanID);
+
+ // Verify the transfer is PENDING initially
+ getAndValidateExternalAssetOwnerTransferByLoan(loanID,
+ ExpectedExternalTransferData.expected(PENDING, firstSaleTransferResponse.getResourceExternalId(), "2023-08-16",
+ "2023-08-16", "9999-12-31", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"),
+ new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"),
+ new BigDecimal("0.000000")));
+
+ // Execute COB job on the next day to activate the transfer
+ updateBusinessDateAndExecuteCOBJob("2023-08-17");
+
+ // Verify the transfer is ACTIVE after COB job
+ getAndValidateExternalAssetOwnerTransferByLoan(loanID,
+ ExpectedExternalTransferData.expected(PENDING, firstSaleTransferResponse.getResourceExternalId(), "2023-08-16",
+ "2023-08-16", "2023-08-16", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"),
+ new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"),
+ new BigDecimal("0.000000")),
+ ExpectedExternalTransferData.expected(ACTIVE, firstSaleTransferResponse.getResourceExternalId(), "2023-08-16",
+ "2023-08-17", "9999-12-31", true, new BigDecimal("15914.980000"), new BigDecimal("15000.000000"),
+ new BigDecimal("757.420000"), new BigDecimal("157.560000"), new BigDecimal("0.000000"),
+ new BigDecimal("0.000000")));
+
+ // Get the owner ID of the first transfer for later verification
+ PageExternalTransferData retrieveResponse = EXTERNAL_ASSET_OWNER_HELPER.retrieveTransfersByLoanId(loanID.longValue());
+ assertNotNull(retrieveResponse.getContent().get(1).getOwner());
+ final String firstOwnerId = retrieveResponse.getContent().get(1).getOwner().getExternalId();
+ assertNull(retrieveResponse.getContent().get(1).getPreviousOwner(), "First sale transfer should not have previous_owner_id");
+
+ // Create buyback transfer
+ updateBusinessDateAndExecuteCOBJob("2023-08-18");
+ final PostInitiateTransferResponse buybackTransferResponse = createBuybackTransfer(loanID, "2023-08-18");
+ validateResponse(buybackTransferResponse, loanID);
+
+ // Execute COB job to process buyback
+ updateBusinessDateAndExecuteCOBJob("2023-08-19");
+
+ // Verify buyback has previous_owner_id set
+ retrieveResponse = EXTERNAL_ASSET_OWNER_HELPER.retrieveTransfersByLoanId(loanID.longValue());
+ assertNotNull(retrieveResponse.getContent().get(2).getPreviousOwner());
+ assertEquals(firstOwnerId, retrieveResponse.getContent().get(2).getPreviousOwner().getExternalId(),
+ "Buyback transfer should have previous_owner_id set to first owner");
+
+ // Run report on settlement date of buyback to check that from_asset_owner_id is populated
+ final Response reportResult = reportHelper.runReport("Transaction Summary Report with Asset Owner",
+ Map.of("R_endDate", "2023-08-18", "R_officeId", officeId.toString(), "output-type", "CSV"));
+
+ assertNotNull(reportResult.body());
+ final String csvContent = reportResult.body().string();
+ final JsonPath jsonPath = JsonPath.from(csvContent);
+
+ // Find Asset Buyback entries to verify from_asset_owner_id is populated
+ final List