FINERACT-2198: Extend Manual Journal entry to support Asset Externalization

This commit is contained in:
Jose Alberto Hernandez
2025-07-07 13:59:33 +02:00
committed by Adam Saghy
parent 4feee70a7a
commit 0c1ca2baba
16 changed files with 190 additions and 21 deletions
@@ -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")
@@ -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;
@@ -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) {
@@ -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);
}
@@ -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);
}
/**
@@ -0,0 +1,34 @@
/**
* 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.exception;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
public class ExternalAssetOwnerNotFoundException extends AbstractPlatformResourceNotFoundException {
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());
}
public ExternalAssetOwnerNotFoundException(Long id) {
super("error.msg.external.asset.owner.id", String.format("External asset owner with id: %s does not found", id), id);
}
}
@@ -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);
}
@@ -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;
}
@@ -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;
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -1301,5 +1301,4 @@ public class AccountingScenarioIntegrationTest {
.withSubmittedDate("01 Jan 2016").withApplicationDate("01 Jan 2016").withRequestedShares("100").build();
return ShareAccountTransactionHelper.createShareAccount(shareAccountJSON, requestSpec, responseSpec);
}
}
@@ -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));
}
}
@@ -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");