This is an automated email from the ASF dual-hosted git repository.

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

commit 0c1ca2baba99528f85e56cef336d4b140a58f893
Author: Jose Alberto Hernandez <[email protected]>
AuthorDate: Wed Jun 18 10:02:40 2025 -0500

    FINERACT-2198: Extend Manual Journal entry to support Asset Externalization
---
 .../journalentry/JournalEntryMapper.java           |  1 +
 .../api/JournalEntryJsonInputParams.java           |  3 +-
 .../journalentry/command/JournalEntryCommand.java  |  4 ++
 .../journalentry/data/JournalEntryData.java        |  8 ++-
 ...JournalEntryCommandFromApiJsonDeserializer.java |  5 +-
 .../ExternalAssetOwnerNotFoundException.java}      | 18 +++---
 .../investor/service/AccountingService.java        |  3 +
 .../investor/service/AccountingServiceImpl.java    |  3 +-
 .../api/JournalEntriesApiResourceSwagger.java      |  2 +
 .../JournalEntryReadPlatformServiceImpl.java       | 12 ++--
 ...ournalEntryRunningBalanceUpdateServiceImpl.java |  2 +-
 ...EntryWritePlatformServiceJpaRepositoryImpl.java | 44 ++++++++++---
 .../AccountingJournalEntryConfiguration.java       | 10 ++-
 .../AccountingScenarioIntegrationTest.java         |  1 -
 .../common/accounting/JournalEntryHelper.java      |  6 ++
 .../InitiateExternalAssetOwnerTransferTest.java    | 73 ++++++++++++++++++++++
 16 files changed, 167 insertions(+), 28 deletions(-)

diff --git 
a/fineract-accounting/src/main/java/org/apache/fineract/accounting/journalentry/JournalEntryMapper.java
 
b/fineract-accounting/src/main/java/org/apache/fineract/accounting/journalentry/JournalEntryMapper.java
index 30f6ac22bb..1f35e6c48b 100644
--- 
a/fineract-accounting/src/main/java/org/apache/fineract/accounting/journalentry/JournalEntryMapper.java
+++ 
b/fineract-accounting/src/main/java/org/apache/fineract/accounting/journalentry/JournalEntryMapper.java
@@ -71,6 +71,7 @@ public interface JournalEntryMapper {
     @Mapping(target = "debits", ignore = true)
     @Mapping(target = "transactionDetails", ignore = true)
     @Mapping(target = "savingTransactionId", ignore = true)
+    @Mapping(target = "externalAssetOwner", ignore = true)
     JournalEntryData map(JournalEntry journalEntry);
 
     @Named("entityType")
diff --git 
a/fineract-accounting/src/main/java/org/apache/fineract/accounting/journalentry/api/JournalEntryJsonInputParams.java
 
b/fineract-accounting/src/main/java/org/apache/fineract/accounting/journalentry/api/JournalEntryJsonInputParams.java
index 2dc5e93665..95b74783c1 100644
--- 
a/fineract-accounting/src/main/java/org/apache/fineract/accounting/journalentry/api/JournalEntryJsonInputParams.java
+++ 
b/fineract-accounting/src/main/java/org/apache/fineract/accounting/journalentry/api/JournalEntryJsonInputParams.java
@@ -43,7 +43,8 @@ public enum JournalEntryJsonInputParams {
     CHECK_NUMBER("checkNumber"), //
     ROUTING_CODE("routingCode"), //
     RECEIPT_NUMBER("receiptNumber"), //
-    BANK_NUMBER("bankNumber"); //
+    BANK_NUMBER("bankNumber"), //
+    EXTERNAL_ASSET_OWNER("externalAssetOwner"); //
 
     private final String value;
 
diff --git 
a/fineract-accounting/src/main/java/org/apache/fineract/accounting/journalentry/command/JournalEntryCommand.java
 
b/fineract-accounting/src/main/java/org/apache/fineract/accounting/journalentry/command/JournalEntryCommand.java
index a50b809d97..45177d8b16 100644
--- 
a/fineract-accounting/src/main/java/org/apache/fineract/accounting/journalentry/command/JournalEntryCommand.java
+++ 
b/fineract-accounting/src/main/java/org/apache/fineract/accounting/journalentry/command/JournalEntryCommand.java
@@ -54,6 +54,7 @@ public class JournalEntryCommand {
     private final SingleDebitOrCreditEntryCommand[] debits;
     private final String locale;
     private final String dateFormat;
+    private final String externalAssetOwner;
 
     public void validateForCreate() {
 
@@ -75,6 +76,9 @@ public class JournalEntryCommand {
 
         
baseDataValidator.reset().parameter("paymentTypeId").value(this.paymentTypeId).ignoreIfNull().longGreaterThanZero();
 
+        
baseDataValidator.reset().parameter(JournalEntryJsonInputParams.EXTERNAL_ASSET_OWNER.getValue()).value(this.externalAssetOwner)
+                .ignoreIfNull().notExceedingLengthOf(100);
+
         // validation for credit array elements
         if (this.credits != null) {
             if (this.credits.length == 0) {
diff --git 
a/fineract-accounting/src/main/java/org/apache/fineract/accounting/journalentry/data/JournalEntryData.java
 
b/fineract-accounting/src/main/java/org/apache/fineract/accounting/journalentry/data/JournalEntryData.java
index c22e096161..db3197312d 100644
--- 
a/fineract-accounting/src/main/java/org/apache/fineract/accounting/journalentry/data/JournalEntryData.java
+++ 
b/fineract-accounting/src/main/java/org/apache/fineract/accounting/journalentry/data/JournalEntryData.java
@@ -94,6 +94,7 @@ public class JournalEntryData {
     private String routingCode;
     private String receiptNumber;
     private String bankNumber;
+    private String externalAssetOwner;
     private transient Long savingTransactionId;
 
     public JournalEntryData() {}
@@ -191,7 +192,7 @@ public class JournalEntryData {
             final EnumOptionData entityType, final Long entityId, final Long 
createdByUserId, final LocalDate submittedOnDate,
             final String createdByUserName, final String comments, final 
Boolean reversed, final String referenceNumber,
             final BigDecimal officeRunningBalance, final BigDecimal 
organizationRunningBalance, final Boolean runningBalanceComputed,
-            final TransactionDetailData transactionDetailData, final 
CurrencyData currency) {
+            final TransactionDetailData transactionDetailData, final 
CurrencyData currency, final String externalAssetOwner) {
         this.id = id;
         this.officeId = officeId;
         this.officeName = officeName;
@@ -218,6 +219,7 @@ public class JournalEntryData {
         this.runningBalanceComputed = runningBalanceComputed;
         this.transactionDetails = transactionDetailData;
         this.currency = currency;
+        this.externalAssetOwner = externalAssetOwner;
     }
 
     public static JournalEntryData importInstance(Long officeId, LocalDate 
transactionDate, String currencyCode, Long paymentTypeId,
@@ -259,10 +261,11 @@ public class JournalEntryData {
         final Boolean runningBalanceComputed = null;
         final TransactionDetailData transactionDetailData = null;
         final CurrencyData currency = null;
+        final String externalAssetOwner = null;
         return new JournalEntryData(id, officeId, officeName, glAccountName, 
glAccountId, glAccountCode, glAccountClassification,
                 transactionDate, entryType, amount, transactionId, 
manualEntry, entityType, entityId, createdByUserId, submittedOnDate,
                 createdByUserName, comments, reversed, referenceNumber, 
officeRunningBalance, organizationRunningBalance,
-                runningBalanceComputed, transactionDetailData, currency);
+                runningBalanceComputed, transactionDetailData, currency, 
externalAssetOwner);
     }
 
     public Integer getRowIndex() {
@@ -274,7 +277,6 @@ public class JournalEntryData {
     }
 
     public void addDebits(CreditDebit debit) {
-
         this.debits.add(debit);
     }
 
diff --git 
a/fineract-accounting/src/main/java/org/apache/fineract/accounting/journalentry/serialization/JournalEntryCommandFromApiJsonDeserializer.java
 
b/fineract-accounting/src/main/java/org/apache/fineract/accounting/journalentry/serialization/JournalEntryCommandFromApiJsonDeserializer.java
index 725225c911..3acc08691d 100644
--- 
a/fineract-accounting/src/main/java/org/apache/fineract/accounting/journalentry/serialization/JournalEntryCommandFromApiJsonDeserializer.java
+++ 
b/fineract-accounting/src/main/java/org/apache/fineract/accounting/journalentry/serialization/JournalEntryCommandFromApiJsonDeserializer.java
@@ -84,6 +84,8 @@ public final class JournalEntryCommandFromApiJsonDeserializer 
extends AbstractFr
                 element);
         final String bankNumber = 
this.fromApiJsonHelper.extractStringNamed(JournalEntryJsonInputParams.BANK_NUMBER.getValue(),
 element);
         final String routingCode = 
this.fromApiJsonHelper.extractStringNamed(JournalEntryJsonInputParams.ROUTING_CODE.getValue(),
 element);
+        final String externalAssetOwner = this.fromApiJsonHelper
+                
.extractStringNamed(JournalEntryJsonInputParams.EXTERNAL_ASSET_OWNER.getValue(),
 element);
 
         SingleDebitOrCreditEntryCommand[] credits = null;
         SingleDebitOrCreditEntryCommand[] debits = null;
@@ -99,7 +101,8 @@ public final class 
JournalEntryCommandFromApiJsonDeserializer extends AbstractFr
         }
         String dateFormat = 
this.fromApiJsonHelper.extractStringNamed(JournalEntryJsonInputParams.DATE_FORMAT.getValue(),
 element);
         return new JournalEntryCommand(officeId, currencyCode, 
transactionDate, comments, referenceNumber, accountingRuleId, amount,
-                paymentTypeId, accountNumber, checkNumber, receiptNumber, 
bankNumber, routingCode, credits, debits, localeStr, dateFormat);
+                paymentTypeId, accountNumber, checkNumber, receiptNumber, 
bankNumber, routingCode, credits, debits, localeStr, dateFormat,
+                externalAssetOwner);
     }
 
     /**
diff --git 
a/fineract-investor/src/main/java/org/apache/fineract/investor/service/AccountingService.java
 
b/fineract-investor/src/main/java/org/apache/fineract/investor/exception/ExternalAssetOwnerNotFoundException.java
old mode 100755
new mode 100644
similarity index 50%
copy from 
fineract-investor/src/main/java/org/apache/fineract/investor/service/AccountingService.java
copy to 
fineract-investor/src/main/java/org/apache/fineract/investor/exception/ExternalAssetOwnerNotFoundException.java
index 08f5615fae..67f8094a68
--- 
a/fineract-investor/src/main/java/org/apache/fineract/investor/service/AccountingService.java
+++ 
b/fineract-investor/src/main/java/org/apache/fineract/investor/exception/ExternalAssetOwnerNotFoundException.java
@@ -16,15 +16,19 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.fineract.investor.service;
+package org.apache.fineract.investor.exception;
 
-import org.apache.fineract.investor.domain.ExternalAssetOwner;
-import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer;
-import org.apache.fineract.portfolio.loanaccount.domain.Loan;
+import org.apache.fineract.infrastructure.core.domain.ExternalId;
+import 
org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
 
-public interface AccountingService {
+public class ExternalAssetOwnerNotFoundException extends 
AbstractPlatformResourceNotFoundException {
 
-    void createJournalEntriesForSaleAssetTransfer(Loan loan, 
ExternalAssetOwnerTransfer transfer, ExternalAssetOwner previousOwner);
+    public ExternalAssetOwnerNotFoundException(ExternalId externalId) {
+        super("error.msg.external.asset.owner.external.id",
+                String.format("External asset owner with external id: %s does 
not found", externalId.getValue()), externalId.getValue());
+    }
 
-    void createJournalEntriesForBuybackAssetTransfer(Loan loan, 
ExternalAssetOwnerTransfer transfer);
+    public ExternalAssetOwnerNotFoundException(Long id) {
+        super("error.msg.external.asset.owner.id", String.format("External 
asset owner with id: %s does not found", id), id);
+    }
 }
diff --git 
a/fineract-investor/src/main/java/org/apache/fineract/investor/service/AccountingService.java
 
b/fineract-investor/src/main/java/org/apache/fineract/investor/service/AccountingService.java
index 08f5615fae..e7a1e892a0 100755
--- 
a/fineract-investor/src/main/java/org/apache/fineract/investor/service/AccountingService.java
+++ 
b/fineract-investor/src/main/java/org/apache/fineract/investor/service/AccountingService.java
@@ -18,6 +18,7 @@
  */
 package org.apache.fineract.investor.service;
 
+import org.apache.fineract.accounting.journalentry.domain.JournalEntry;
 import org.apache.fineract.investor.domain.ExternalAssetOwner;
 import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer;
 import org.apache.fineract.portfolio.loanaccount.domain.Loan;
@@ -27,4 +28,6 @@ public interface AccountingService {
     void createJournalEntriesForSaleAssetTransfer(Loan loan, 
ExternalAssetOwnerTransfer transfer, ExternalAssetOwner previousOwner);
 
     void createJournalEntriesForBuybackAssetTransfer(Loan loan, 
ExternalAssetOwnerTransfer transfer);
+
+    void createMappingToOwner(ExternalAssetOwner owner, JournalEntry 
journalEntry);
 }
diff --git 
a/fineract-investor/src/main/java/org/apache/fineract/investor/service/AccountingServiceImpl.java
 
b/fineract-investor/src/main/java/org/apache/fineract/investor/service/AccountingServiceImpl.java
index 80425abc79..98dcc22735 100644
--- 
a/fineract-investor/src/main/java/org/apache/fineract/investor/service/AccountingServiceImpl.java
+++ 
b/fineract-investor/src/main/java/org/apache/fineract/investor/service/AccountingServiceImpl.java
@@ -120,7 +120,8 @@ public class AccountingServiceImpl implements 
AccountingService {
         return journalEntryList;
     }
 
-    private void createMappingToOwner(final ExternalAssetOwner owner, final 
JournalEntry journalEntry) {
+    @Override
+    public void createMappingToOwner(final ExternalAssetOwner owner, final 
JournalEntry journalEntry) {
         if (owner == null) {
             return;
         }
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/api/JournalEntriesApiResourceSwagger.java
 
b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/api/JournalEntriesApiResourceSwagger.java
index cc0aa3b0c9..2e15e99b2a 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/api/JournalEntriesApiResourceSwagger.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/api/JournalEntriesApiResourceSwagger.java
@@ -177,6 +177,8 @@ final class JournalEntriesApiResourceSwagger {
         public String createdByUserName;
         @Schema(example = "[2022, 07, 01]")
         public LocalDate createdDate;
+        @Schema(example = "qwerty1234")
+        public String externalAssetOwner;
 
         public CurrencyItem currency;
         public EnumOptionType glAccountType;
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryReadPlatformServiceImpl.java
 
b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryReadPlatformServiceImpl.java
index 021822d0ed..68bf33aa25 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryReadPlatformServiceImpl.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryReadPlatformServiceImpl.java
@@ -99,7 +99,8 @@ public class JournalEntryReadPlatformServiceImpl implements 
JournalEntryReadPlat
                     .append(" creatingUser.username as createdByUserName, 
journalEntry.description as comments, ")
                     .append(" journalEntry.submitted_on_date as 
submittedOnDate, journalEntry.reversed as reversed, ")
                     .append(" journalEntry.currency_code as currencyCode, 
curr.name as currencyName, curr.internationalized_name_code as 
currencyNameCode, ")
-                    .append(" curr.display_symbol as currencyDisplaySymbol, 
curr.decimal_places as currencyDigits, curr.currency_multiplesof as 
inMultiplesOf ");
+                    .append(" curr.display_symbol as currencyDisplaySymbol, 
curr.decimal_places as currencyDigits, curr.currency_multiplesof as 
inMultiplesOf, ")
+                    .append(" eao.external_id as externalAssetOwner ");
             if (associationParametersData.isRunningBalanceRequired()) {
                 sb.append(" ,journalEntry.is_running_balance_calculated as 
runningBalanceComputed, ")
                         .append(" journalEntry.office_running_balance as 
officeRunningBalance, ")
@@ -117,7 +118,9 @@ public class JournalEntryReadPlatformServiceImpl implements 
JournalEntryReadPlat
                     .append(" left join acc_gl_account as glAccount on 
glAccount.id = journalEntry.account_id")
                     .append(" left join m_office as office on office.id = 
journalEntry.office_id")
                     .append(" left join m_appuser as creatingUser on 
creatingUser.id = journalEntry.created_by ")
-                    .append(" join m_currency curr on curr.code = 
journalEntry.currency_code ");
+                    .append(" join m_currency curr on curr.code = 
journalEntry.currency_code ")
+                    .append(" left join 
m_external_asset_owner_journal_entry_mapping eajem on eajem.journal_entry_id = 
journalEntry.id ")
+                    .append(" left join m_external_asset_owner eao on eao.id = 
eajem.owner_id ");
             if (associationParametersData.isTransactionDetailsRequired()) {
                 sb.append(" left join m_loan_transaction as lt on 
journalEntry.loan_transaction_id = lt.id ")
                         .append(" left join m_savings_account_transaction as 
st on journalEntry.savings_transaction_id = st.id ")
@@ -150,7 +153,6 @@ public class JournalEntryReadPlatformServiceImpl implements 
JournalEntryReadPlat
             EnumOptionData entityType = null;
             if (entityTypeId != null) {
                 entityType = 
AccountingEnumerations.portfolioProductType(entityTypeId);
-
             }
 
             final Long entityId = JdbcSupport.getLong(rs, "entityId");
@@ -224,10 +226,12 @@ public class JournalEntryReadPlatformServiceImpl 
implements JournalEntryReadPlat
 
                 transactionDetailData = new TransactionDetailData(transaction, 
paymentDetailData, noteData, transactionTypeEnumData);
             }
+            final String externalAssetOwner = 
rs.getString("externalAssetOwner");
+
             return new JournalEntryData(id, officeId, officeName, 
glAccountName, glAccountId, glCode, accountType, transactionDate,
                     entryType, amount, transactionId, manualEntry, entityType, 
entityId, createdByUserId, submittedOnDate,
                     createdByUserName, comments, reversed, referenceNumber, 
officeRunningBalance, organizationRunningBalance,
-                    runningBalanceComputed, transactionDetailData, currency);
+                    runningBalanceComputed, transactionDetailData, currency, 
externalAssetOwner);
         }
     }
 
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryRunningBalanceUpdateServiceImpl.java
 
b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryRunningBalanceUpdateServiceImpl.java
index 9b669f2cb4..d497f5fc3d 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryRunningBalanceUpdateServiceImpl.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryRunningBalanceUpdateServiceImpl.java
@@ -278,7 +278,7 @@ public class JournalEntryRunningBalanceUpdateServiceImpl 
implements JournalEntry
             final EnumOptionData entryType = 
AccountingEnumerations.journalEntryType(entryTypeId);
 
             return new JournalEntryData(id, officeId, null, null, glAccountId, 
null, accountType, null, entryType, amount, null, null, null,
-                    null, null, null, null, null, null, null, null, null, 
null, null, null);
+                    null, null, null, null, null, null, null, null, null, 
null, null, null, null);
         }
     }
 
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryWritePlatformServiceJpaRepositoryImpl.java
 
b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryWritePlatformServiceJpaRepositoryImpl.java
index 1aec812805..9aeb953642 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryWritePlatformServiceJpaRepositoryImpl.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryWritePlatformServiceJpaRepositoryImpl.java
@@ -27,6 +27,7 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Optional;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
@@ -60,17 +61,25 @@ import 
org.apache.fineract.accounting.provisioning.domain.ProvisioningEntry;
 import org.apache.fineract.accounting.rule.domain.AccountingRule;
 import org.apache.fineract.accounting.rule.domain.AccountingRuleRepository;
 import 
org.apache.fineract.accounting.rule.exception.AccountingRuleNotFoundException;
+import 
org.apache.fineract.infrastructure.configuration.api.GlobalConfigurationConstants;
+import 
org.apache.fineract.infrastructure.configuration.service.ConfigurationReadPlatformService;
 import org.apache.fineract.infrastructure.core.api.JsonCommand;
 import org.apache.fineract.infrastructure.core.data.ApiParameterError;
 import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
 import 
org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
 import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
+import org.apache.fineract.infrastructure.core.domain.ExternalId;
 import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
 import 
org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
 import 
org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
 import 
org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
 import org.apache.fineract.infrastructure.core.service.DateUtils;
+import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
 import 
org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
+import org.apache.fineract.investor.domain.ExternalAssetOwner;
+import org.apache.fineract.investor.domain.ExternalAssetOwnerRepository;
+import 
org.apache.fineract.investor.exception.ExternalAssetOwnerNotFoundException;
+import org.apache.fineract.investor.service.AccountingService;
 import 
org.apache.fineract.organisation.monetary.domain.OrganisationCurrencyRepositoryWrapper;
 import org.apache.fineract.organisation.office.domain.Office;
 import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper;
@@ -105,6 +114,9 @@ public class 
JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa
     private final PaymentDetailWritePlatformService 
paymentDetailWritePlatformService;
     private final FinancialActivityAccountRepositoryWrapper 
financialActivityAccountRepositoryWrapper;
     private final CashBasedAccountingProcessorForClientTransactions 
accountingProcessorForClientTransactions;
+    private final ConfigurationReadPlatformService 
configurationReadPlatformService;
+    private final AccountingService accountingService;
+    private final ExternalAssetOwnerRepository externalAssetOwnerRepository;
 
     @Transactional
     @Override
@@ -131,6 +143,22 @@ public class 
JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa
             final String transactionId = generateTransactionId(officeId);
             final String referenceNumber = 
command.stringValueOfParameterNamed(JournalEntryJsonInputParams.REFERENCE_NUMBER.getValue());
 
+            ExternalAssetOwner externalAssetOwner = null;
+            final ExternalId externalId = ExternalIdFactory
+                    
.produce(command.stringValueOfParameterNamed(JournalEntryJsonInputParams.EXTERNAL_ASSET_OWNER.getValue()));
+            if (!externalId.isEmpty()) {
+                if (!configurationReadPlatformService
+                        
.retrieveGlobalConfiguration(GlobalConfigurationConstants.ASSET_EXTERNALIZATION_OF_NON_ACTIVE_LOANS).isEnabled())
 {
+                    throw new 
JournalEntryRuntimeException("error.msg.glJournalEntry.asset.externalization.not.enabled",
+                            "GL Journal Entry with Asset Externalization not 
enabled");
+                }
+                final Optional<ExternalAssetOwner> optExternalAssetOwner = 
externalAssetOwnerRepository.findByExternalId(externalId);
+                if (!optExternalAssetOwner.isPresent()) {
+                    throw new ExternalAssetOwnerNotFoundException(externalId);
+                }
+                externalAssetOwner = optExternalAssetOwner.get();
+            }
+
             if (accountRuleId != null) {
 
                 final AccountingRule accountingRule = 
this.accountingRuleRepository.findById(accountRuleId)
@@ -147,13 +175,13 @@ public class 
JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa
                     }
 
                     saveAllDebitOrCreditEntries(journalEntryCommand, office, 
paymentDetail, currencyCode, transactionDate,
-                            journalEntryCommand.getCredits(), transactionId, 
JournalEntryType.CREDIT, referenceNumber);
+                            journalEntryCommand.getCredits(), transactionId, 
JournalEntryType.CREDIT, referenceNumber, externalAssetOwner);
                 } else {
                     final GLAccount creditAccountHead = 
accountingRule.getAccountToCredit();
                     validateGLAccountForTransaction(creditAccountHead);
                     
validateDebitOrCreditArrayForExistingGLAccount(creditAccountHead, 
journalEntryCommand.getCredits());
                     saveAllDebitOrCreditEntries(journalEntryCommand, office, 
paymentDetail, currencyCode, transactionDate,
-                            journalEntryCommand.getCredits(), transactionId, 
JournalEntryType.CREDIT, referenceNumber);
+                            journalEntryCommand.getCredits(), transactionId, 
JournalEntryType.CREDIT, referenceNumber, externalAssetOwner);
                 }
 
                 if (accountingRule.getAccountToDebit() == null) {
@@ -167,21 +195,21 @@ public class 
JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa
                     }
 
                     saveAllDebitOrCreditEntries(journalEntryCommand, office, 
paymentDetail, currencyCode, transactionDate,
-                            journalEntryCommand.getDebits(), transactionId, 
JournalEntryType.DEBIT, referenceNumber);
+                            journalEntryCommand.getDebits(), transactionId, 
JournalEntryType.DEBIT, referenceNumber, externalAssetOwner);
                 } else {
                     final GLAccount debitAccountHead = 
accountingRule.getAccountToDebit();
                     validateGLAccountForTransaction(debitAccountHead);
                     
validateDebitOrCreditArrayForExistingGLAccount(debitAccountHead, 
journalEntryCommand.getDebits());
                     saveAllDebitOrCreditEntries(journalEntryCommand, office, 
paymentDetail, currencyCode, transactionDate,
-                            journalEntryCommand.getDebits(), transactionId, 
JournalEntryType.DEBIT, referenceNumber);
+                            journalEntryCommand.getDebits(), transactionId, 
JournalEntryType.DEBIT, referenceNumber, externalAssetOwner);
                 }
             } else {
 
                 saveAllDebitOrCreditEntries(journalEntryCommand, office, 
paymentDetail, currencyCode, transactionDate,
-                        journalEntryCommand.getDebits(), transactionId, 
JournalEntryType.DEBIT, referenceNumber);
+                        journalEntryCommand.getDebits(), transactionId, 
JournalEntryType.DEBIT, referenceNumber, externalAssetOwner);
 
                 saveAllDebitOrCreditEntries(journalEntryCommand, office, 
paymentDetail, currencyCode, transactionDate,
-                        journalEntryCommand.getCredits(), transactionId, 
JournalEntryType.CREDIT, referenceNumber);
+                        journalEntryCommand.getCredits(), transactionId, 
JournalEntryType.CREDIT, referenceNumber, externalAssetOwner);
 
             }
 
@@ -598,7 +626,7 @@ public class 
JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa
     private void saveAllDebitOrCreditEntries(final JournalEntryCommand 
command, final Office office, final PaymentDetail paymentDetail,
             final String currencyCode, final LocalDate transactionDate,
             final SingleDebitOrCreditEntryCommand[] 
singleDebitOrCreditEntryCommands, final String transactionId,
-            final JournalEntryType type, final String referenceNumber) {
+            final JournalEntryType type, final String referenceNumber, final 
ExternalAssetOwner externalAssetOwner) {
         final boolean manualEntry = true;
         for (final SingleDebitOrCreditEntryCommand 
singleDebitOrCreditEntryCommand : singleDebitOrCreditEntryCommands) {
             final GLAccount glAccount = 
this.glAccountRepository.findById(singleDebitOrCreditEntryCommand.getGlAccountId())
@@ -618,6 +646,8 @@ public class 
JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa
                     manualEntry, transactionDate, type, 
singleDebitOrCreditEntryCommand.getAmount(), comments, null, null, 
referenceNumber,
                     null, null, null, null);
             helper.persistJournalEntry(glJournalEntry);
+
+            accountingService.createMappingToOwner(externalAssetOwner, 
glJournalEntry);
         }
     }
 
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/starter/AccountingJournalEntryConfiguration.java
 
b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/starter/AccountingJournalEntryConfiguration.java
index c22fbf3943..397e486cef 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/starter/AccountingJournalEntryConfiguration.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/starter/AccountingJournalEntryConfiguration.java
@@ -35,11 +35,14 @@ import 
org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlat
 import 
org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformServiceJpaRepositoryImpl;
 import 
org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMappingRepository;
 import org.apache.fineract.accounting.rule.domain.AccountingRuleRepository;
+import 
org.apache.fineract.infrastructure.configuration.service.ConfigurationReadPlatformService;
 import org.apache.fineract.infrastructure.core.service.PaginationHelper;
 import 
org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
 import 
org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
 import 
org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
 import org.apache.fineract.infrastructure.security.utils.ColumnValidator;
+import org.apache.fineract.investor.domain.ExternalAssetOwnerRepository;
+import org.apache.fineract.investor.service.AccountingService;
 import 
org.apache.fineract.organisation.monetary.domain.OrganisationCurrencyRepositoryWrapper;
 import org.apache.fineract.organisation.office.domain.OfficeRepository;
 import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper;
@@ -89,11 +92,14 @@ public class AccountingJournalEntryConfiguration {
             GLAccountReadPlatformService glAccountReadPlatformService, 
OrganisationCurrencyRepositoryWrapper organisationCurrencyRepository,
             PlatformSecurityContext context, PaymentDetailWritePlatformService 
paymentDetailWritePlatformService,
             FinancialActivityAccountRepositoryWrapper 
financialActivityAccountRepositoryWrapper,
-            CashBasedAccountingProcessorForClientTransactions 
accountingProcessorForClientTransactions) {
+            CashBasedAccountingProcessorForClientTransactions 
accountingProcessorForClientTransactions,
+            ConfigurationReadPlatformService configurationReadPlatformService, 
AccountingService accountingService,
+            ExternalAssetOwnerRepository externalAssetOwnerRepository) {
         return new 
JournalEntryWritePlatformServiceJpaRepositoryImpl(glClosureRepository, 
glAccountRepository, glJournalEntryRepository,
                 officeRepositoryWrapper, accountingProcessorForLoanFactory, 
accountingProcessorForSavingsFactory,
                 accountingProcessorForSharesFactory, helper, 
fromApiJsonDeserializer, accountingRuleRepository,
                 glAccountReadPlatformService, organisationCurrencyRepository, 
context, paymentDetailWritePlatformService,
-                financialActivityAccountRepositoryWrapper, 
accountingProcessorForClientTransactions);
+                financialActivityAccountRepositoryWrapper, 
accountingProcessorForClientTransactions, configurationReadPlatformService,
+                accountingService, externalAssetOwnerRepository);
     }
 }
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AccountingScenarioIntegrationTest.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AccountingScenarioIntegrationTest.java
index 47a80859b3..fa529610ff 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AccountingScenarioIntegrationTest.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AccountingScenarioIntegrationTest.java
@@ -1301,5 +1301,4 @@ public class AccountingScenarioIntegrationTest {
                 .withSubmittedDate("01 Jan 2016").withApplicationDate("01 Jan 
2016").withRequestedShares("100").build();
         return 
ShareAccountTransactionHelper.createShareAccount(shareAccountJSON, requestSpec, 
responseSpec);
     }
-
 }
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/accounting/JournalEntryHelper.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/accounting/JournalEntryHelper.java
index cf52c99cec..62744304c6 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/accounting/JournalEntryHelper.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/accounting/JournalEntryHelper.java
@@ -192,4 +192,10 @@ public class JournalEntryHelper {
     public static PostJournalEntriesResponse createJournalEntry(String 
command, JournalEntryCommand request) {
         return 
Calls.ok(FineractClientHelper.getFineractClient().journalEntries.createGLJournalEntry(command,
 request));
     }
+
+    public static GetJournalEntriesTransactionIdResponse 
retrieveJournalEntryByTransactionId(final String transactionId) {
+        return 
Calls.ok(FineractClientHelper.getFineractClient().journalEntries.retrieveAll1(//
+                null, null, null, null, null, null, null, transactionId, null, 
//
+                null, null, null, null, null, null, null, null, null, true));
+    }
 }
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 327d2a6959..15e9aca31f 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
@@ -60,11 +60,20 @@ import 
org.apache.fineract.client.models.ExternalOwnerJournalEntryData;
 import org.apache.fineract.client.models.ExternalOwnerTransferJournalEntryData;
 import org.apache.fineract.client.models.ExternalTransferData;
 import org.apache.fineract.client.models.GetFinancialActivityAccountsResponse;
+import 
org.apache.fineract.client.models.GetJournalEntriesTransactionIdResponse;
+import org.apache.fineract.client.models.JournalEntryCommand;
+import org.apache.fineract.client.models.JournalEntryTransactionItem;
 import org.apache.fineract.client.models.PageExternalTransferData;
 import org.apache.fineract.client.models.PostFinancialActivityAccountsRequest;
 import org.apache.fineract.client.models.PostInitiateTransferResponse;
+import org.apache.fineract.client.models.PostJournalEntriesResponse;
+import org.apache.fineract.client.models.PostLoanProductsResponse;
+import org.apache.fineract.client.models.PostLoansLoanIdRequest;
 import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest;
+import org.apache.fineract.client.models.PostLoansRequest;
+import org.apache.fineract.client.models.PostLoansResponse;
 import org.apache.fineract.client.models.PutGlobalConfigurationsRequest;
+import org.apache.fineract.client.models.SingleDebitOrCreditEntryCommand;
 import org.apache.fineract.client.util.CallFailedRuntimeException;
 import 
org.apache.fineract.infrastructure.configuration.api.GlobalConfigurationConstants;
 import 
org.apache.fineract.infrastructure.event.external.data.ExternalEventResponse;
@@ -80,6 +89,7 @@ import org.apache.fineract.integrationtests.common.Utils;
 import org.apache.fineract.integrationtests.common.accounting.Account;
 import org.apache.fineract.integrationtests.common.accounting.AccountHelper;
 import 
org.apache.fineract.integrationtests.common.accounting.FinancialActivityAccountHelper;
+import 
org.apache.fineract.integrationtests.common.accounting.JournalEntryHelper;
 import org.apache.fineract.integrationtests.common.charges.ChargesHelper;
 import 
org.apache.fineract.integrationtests.common.externalevents.ExternalEventHelper;
 import 
org.apache.fineract.integrationtests.common.externalevents.ExternalEventsExtension;
@@ -1178,6 +1188,69 @@ public class InitiateExternalAssetOwnerTransferTest 
extends BaseLoanIntegrationT
         }
     }
 
+    @Test
+    public void addManualJournalEntriesWithAssetExternalization() {
+        runAt("10 April 2025", () -> {
+
+            final Account glAccountDebit = accountHelper.createAssetAccount();
+            final Account glAccountCredit = 
accountHelper.createLiabilityAccount();
+            final String externalAssetOwner = 
Utils.uniqueRandomStringGenerator("ASSET_EXTERNAL_", 5);
+
+            CallFailedRuntimeException callFailedRuntimeException = 
Assertions.assertThrows(CallFailedRuntimeException.class,
+                    () -> JournalEntryHelper.createJournalEntry("", new 
JournalEntryCommand().amount(BigDecimal.TEN).officeId(1L)
+                            
.currencyCode("USD").locale("en").dateFormat("uuuu-MM-dd").transactionDate(LocalDate.of(2024,
 1, 1))
+                            .addCreditsItem(new 
SingleDebitOrCreditEntryCommand().glAccountId(glAccountDebit.getAccountID().longValue())
+                                    .amount(BigDecimal.TEN))
+                            .addDebitsItem(new 
SingleDebitOrCreditEntryCommand().glAccountId(glAccountCredit.getAccountID().longValue())
+                                    .amount(BigDecimal.TEN))
+                            .externalAssetOwner(externalAssetOwner)));
+            
Assertions.assertTrue(callFailedRuntimeException.getMessage().contains("External
 asset owner with external id:"));
+
+            final Integer clientId = ClientHelper.createClient(requestSpec, 
responseSpec);
+            final String operationDate = "10 April 2025";
+
+            PostLoanProductsResponse loanProductResponse = 
loanProductHelper.createLoanProduct(
+                    
createOnePeriod30DaysPeriodicAccrualProductWithAdvancedPaymentAllocationAndInterestRecalculation(12.0,
 4));
+
+            final PostLoansRequest applicationRequest = 
applyLoanRequest(clientId.longValue(), loanProductResponse.getResourceId(),
+                    operationDate, 1000.0, 
4).transactionProcessingStrategyCode("advanced-payment-allocation-strategy")//
+                    .interestRatePerPeriod(BigDecimal.valueOf(12.0));
+
+            final PostLoansResponse loanResponse = 
loanTransactionHelper.applyLoan(applicationRequest);
+            final Long loanId = loanResponse.getLoanId();
+
+            loanTransactionHelper.approveLoan(loanId, new 
PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(1000.0))
+                    
.dateFormat(DATETIME_PATTERN).approvedOnDate(operationDate).locale("en"));
+
+            loanTransactionHelper.disburseLoan(loanId, new 
PostLoansLoanIdRequest().actualDisbursementDate(operationDate)
+                    
.dateFormat(DATETIME_PATTERN).transactionAmount(BigDecimal.valueOf(1000.0)).locale("en"));
+
+            PostInitiateTransferResponse transferResponse = 
EXTERNAL_ASSET_OWNER_HELPER.initiateTransferByLoanId(loanId, "sale",
+                    new 
ExternalAssetOwnerRequest().settlementDate("2025-04-20").dateFormat("yyyy-MM-dd").locale("en")
+                            
.transferExternalId(externalAssetOwner).transferExternalGroupId(null).ownerExternalId(externalAssetOwner)
+                            .purchasePriceRatio("0.90"));
+            assertEquals(externalAssetOwner, 
transferResponse.getResourceExternalId());
+
+            final PostJournalEntriesResponse journalEntriesResponse = 
JournalEntryHelper.createJournalEntry("",
+                    new 
JournalEntryCommand().amount(BigDecimal.TEN).officeId(1L).currencyCode("USD").locale("en").dateFormat("uuuu-MM-dd")
+                            .transactionDate(LocalDate.of(2024, 1, 1))
+                            .addCreditsItem(new 
SingleDebitOrCreditEntryCommand().glAccountId(glAccountDebit.getAccountID().longValue())
+                                    .amount(BigDecimal.TEN))
+                            .addDebitsItem(new 
SingleDebitOrCreditEntryCommand().glAccountId(glAccountCredit.getAccountID().longValue())
+                                    .amount(BigDecimal.TEN))
+                            .externalAssetOwner(externalAssetOwner));
+
+            final GetJournalEntriesTransactionIdResponse 
journalEntriesTransactionIdResponse = JournalEntryHelper
+                    
.retrieveJournalEntryByTransactionId(journalEntriesResponse.getTransactionId());
+            Assertions.assertNotNull(journalEntriesTransactionIdResponse);
+            assertEquals(2, 
journalEntriesTransactionIdResponse.getPageItems().size());
+            JournalEntryTransactionItem journalEntryItem = 
journalEntriesTransactionIdResponse.getPageItems().get(0);
+            assertEquals(externalAssetOwner, 
journalEntryItem.getExternalAssetOwner());
+            journalEntryItem = 
journalEntriesTransactionIdResponse.getPageItems().get(1);
+            assertEquals(externalAssetOwner, 
journalEntryItem.getExternalAssetOwner());
+        });
+    }
+
     private void updateBusinessDateAndExecuteCOBJob(String date) {
         BusinessDateHelper.updateBusinessDate(REQUEST_SPEC, RESPONSE_SPEC, 
BUSINESS_DATE, LocalDate.parse(date));
         SCHEDULER_JOB_HELPER.executeAndAwaitJob("Loan COB");


Reply via email to