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");
