FINERACT-1926: External owner accounting

This commit is contained in:
Adam Saghy
2023-06-12 11:24:31 +02:00
committed by Arnold Gálovics
parent 437b8e9cae
commit 582f36ad34
416 changed files with 1952 additions and 613 deletions
@@ -0,0 +1,140 @@
/**
* 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.accounting.journalentry.service;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.accounting.closure.domain.GLClosure;
import org.apache.fineract.accounting.closure.domain.GLClosureRepository;
import org.apache.fineract.accounting.common.AccountingConstants;
import org.apache.fineract.accounting.common.AccountingConstants.FinancialActivity;
import org.apache.fineract.accounting.financialactivityaccount.domain.FinancialActivityAccount;
import org.apache.fineract.accounting.financialactivityaccount.domain.FinancialActivityAccountRepositoryWrapper;
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
import org.apache.fineract.accounting.journalentry.domain.JournalEntry;
import org.apache.fineract.accounting.journalentry.domain.JournalEntryRepository;
import org.apache.fineract.accounting.journalentry.domain.JournalEntryType;
import org.apache.fineract.accounting.journalentry.exception.JournalEntryInvalidException;
import org.apache.fineract.accounting.journalentry.exception.JournalEntryInvalidException.GlJournalEntryInvalidReason;
import org.apache.fineract.accounting.producttoaccountmapping.domain.PortfolioProductType;
import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMapping;
import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMappingRepository;
import org.apache.fineract.accounting.producttoaccountmapping.exception.ProductToGLAccountMappingNotFoundException;
import org.apache.fineract.organisation.office.domain.Office;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class InvestorAccountingHelper {
public static final String INVESTOR_TRANSFER_IDENTIFIER = "I";
private final JournalEntryRepository glJournalEntryRepository;
private final ProductToGLAccountMappingRepository accountMappingRepository;
private final FinancialActivityAccountRepositoryWrapper financialActivityAccountRepository;
private final GLClosureRepository closureRepository;
/**
* @param officeId
* @param transactionDate
*/
public void checkForBranchClosures(Long officeId, final LocalDate transactionDate) {
/**
* check if an accounting closure has happened for this branch after the transaction Date
**/
GLClosure gLClosure = getLatestClosureByBranch(officeId);
if (gLClosure != null) {
if (gLClosure.getClosingDate().isAfter(transactionDate) || gLClosure.getClosingDate().isEqual(transactionDate)) {
throw new JournalEntryInvalidException(GlJournalEntryInvalidReason.ACCOUNTING_CLOSED, gLClosure.getClosingDate(), null,
null);
}
}
}
public JournalEntry createDebitJournalEntryOrReversalForInvestor(final Office office, final String currencyCode,
final int accountMappingTypeId, final Long loanProductId, final Long loanId, final Long transactionId,
final LocalDate transactionDate, final BigDecimal amount, final Boolean isReversalOrder) {
final GLAccount account = getLinkedGLAccountForLoanProduct(loanProductId, accountMappingTypeId);
if (isReversalOrder) {
return createCreditJournalEntryForInvestor(office, currencyCode, account, loanId, transactionId, transactionDate, amount);
} else {
return createDebitJournalEntryForInvestor(office, currencyCode, account, loanId, transactionId, transactionDate, amount);
}
}
public JournalEntry createCreditJournalEntryOrReversalForInvestor(final Office office, final String currencyCode, final Long loanId,
final Long transactionId, final LocalDate transactionDate, final BigDecimal amount, final Boolean isReversalOrder,
final GLAccount account) {
if (isReversalOrder) {
return createDebitJournalEntryForInvestor(office, currencyCode, account, loanId, transactionId, transactionDate, amount);
} else {
return createCreditJournalEntryForInvestor(office, currencyCode, account, loanId, transactionId, transactionDate, amount);
}
}
private JournalEntry createCreditJournalEntryForInvestor(final Office office, final String currencyCode, final GLAccount account,
final Long loanId, final Long transactionId, final LocalDate transactionDate, final BigDecimal amount) {
final boolean manualEntry = false;
final String modifiedTransactionId = INVESTOR_TRANSFER_IDENTIFIER + transactionId;
final JournalEntry journalEntry = JournalEntry.createNew(office, null, account, currencyCode, modifiedTransactionId, manualEntry,
transactionDate, JournalEntryType.CREDIT, amount, null, PortfolioProductType.LOAN.getValue(), loanId, null, null, null,
null, null);
return this.glJournalEntryRepository.saveAndFlush(journalEntry);
}
private JournalEntry createDebitJournalEntryForInvestor(final Office office, final String currencyCode, final GLAccount account,
final Long loanId, final Long transactionId, final LocalDate transactionDate, final BigDecimal amount) {
final boolean manualEntry = false;
String modifiedTransactionId = INVESTOR_TRANSFER_IDENTIFIER + transactionId;
final JournalEntry journalEntry = JournalEntry.createNew(office, null, account, currencyCode, modifiedTransactionId, manualEntry,
transactionDate, JournalEntryType.DEBIT, amount, null, PortfolioProductType.LOAN.getValue(), loanId, null, null, null, null,
null);
return this.glJournalEntryRepository.saveAndFlush(journalEntry);
}
public GLAccount getLinkedGLAccountForLoanProduct(final Long loanProductId, final int accountMappingTypeId) {
GLAccount glAccount;
if (isOrganizationAccount(accountMappingTypeId)) {
FinancialActivityAccount financialActivityAccount = this.financialActivityAccountRepository
.findByFinancialActivityTypeWithNotFoundDetection(accountMappingTypeId);
glAccount = financialActivityAccount.getGlAccount();
} else {
ProductToGLAccountMapping accountMapping = this.accountMappingRepository.findCoreProductToFinAccountMapping(loanProductId,
PortfolioProductType.LOAN.getValue(), accountMappingTypeId);
if (accountMapping == null) {
throw new ProductToGLAccountMappingNotFoundException(PortfolioProductType.LOAN, loanProductId,
AccountingConstants.AccrualAccountsForLoan.fromInt(accountMappingTypeId).toString());
}
glAccount = accountMapping.getGlAccount();
}
return glAccount;
}
private boolean isOrganizationAccount(final int accountMappingTypeId) {
return FinancialActivity.fromInt(accountMappingTypeId) != null;
}
public GLClosure getLatestClosureByBranch(final long officeId) {
return this.closureRepository.getLatestGLClosureByBranch(officeId);
}
}
@@ -40,17 +40,19 @@ import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.service.CommandWrapperBuilder;
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException;
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import org.apache.fineract.infrastructure.core.service.CommandParameterUtil;
import org.apache.fineract.infrastructure.security.service.PlatformUserRightsContext;
import org.apache.fineract.investor.config.InvestorModuleIsEnabledCondition;
import org.apache.fineract.investor.data.ExternalOwnerJournalEntryData;
import org.apache.fineract.investor.data.ExternalOwnerTransferJournalEntryData;
import org.apache.fineract.investor.data.ExternalTransferData;
import org.apache.fineract.investor.data.ExternalTransferResponseData;
import org.apache.fineract.investor.service.ExternalAssetOwnersReadService;
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformServiceCommon;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Conditional;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Component;
@@ -58,13 +60,11 @@ import org.springframework.stereotype.Component;
@Component
@Tag(name = "External Asset Owners", description = "External Asset Owners")
@RequiredArgsConstructor
@ConditionalOnProperty(value = "fineract.module.external-asset-owner.enabled", havingValue = "true")
@Conditional(InvestorModuleIsEnabledCondition.class)
public class ExternalAssetOwnersApiResource {
private final PlatformUserRightsContext platformUserRightsContext;
private final ExternalAssetOwnersReadService externalAssetOwnersReadService;
private final ApiRequestParameterHelper apiRequestParameterHelper;
private final DefaultToApiJsonSerializer<ExternalTransferData> apiJsonSerializerService;
private final DefaultToApiJsonSerializer<ExternalTransferResponseData> postApiJsonSerializerService;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
private final LoanReadPlatformServiceCommon loanReadPlatformService;
@@ -106,7 +106,7 @@ public class ExternalAssetOwnersApiResource {
@Produces({ MediaType.APPLICATION_JSON })
@Operation(tags = {
"External Asset Owners" }, summary = "Retrieve External Asset Owner Transfers", description = "Retrieve External Asset Owner Transfer items by transferExternalId, loanId or loanExternalId")
public Page<ExternalTransferData> getTransfer(
public Page<ExternalTransferData> getTransfers(
@QueryParam("transferExternalId") @Parameter(description = "transferExternalId") final String transferExternalId,
@QueryParam("loanId") @Parameter(description = "loanId") final Long loanId,
@QueryParam("loanExternalId") @Parameter(description = "loanExternalId") final String loanExternalId,
@@ -117,6 +117,49 @@ public class ExternalAssetOwnersApiResource {
}
@GET
@Path("/transfers/active-transfer")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(tags = {
"External Asset Owners" }, summary = "Retrieve Active Asset Owner Transfer", description = "Retrieve Active External Asset Owner Transfer by transferExternalId, loanId or loanExternalId")
public ExternalTransferData getActiveTransfer(
@QueryParam("transferExternalId") @Parameter(description = "transferExternalId") final String transferExternalId,
@QueryParam("loanId") @Parameter(description = "loanId") final Long loanId,
@QueryParam("loanExternalId") @Parameter(description = "loanExternalId") final String loanExternalId,
@Context final UriInfo uriInfo) {
platformUserRightsContext.isAuthenticated();
return externalAssetOwnersReadService.retrieveActiveTransferData(loanId, loanExternalId, transferExternalId);
}
@GET
@Path("/transfers/{transferId}/journal-entries")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(tags = {
"External Asset Owners" }, summary = "Retrieve Journal Entries of Transfer", description = "Retrieve Journal entries of transfer by transferId")
public ExternalOwnerTransferJournalEntryData getJournalEntriesOfTransfer(
@PathParam("transferId") @Parameter(description = "transferId") final Long transferId,
@QueryParam("offset") @Parameter(description = "offset") final Integer offset,
@QueryParam("limit") @Parameter(description = "limit") final Integer limit, @Context final UriInfo uriInfo) {
platformUserRightsContext.isAuthenticated();
return externalAssetOwnersReadService.retrieveJournalEntriesOfTransfer(transferId, offset, limit);
}
@GET
@Path("/owners/external-id/{ownerExternalId}/journal-entries")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(tags = {
"External Asset Owners" }, summary = "Retrieve Journal Entries of Owner", description = "Retrieve Journal entries of owner by owner externalId")
public ExternalOwnerJournalEntryData getJournalEntriesOfOwner(
@PathParam("ownerExternalId") @Parameter(description = "ownerExternalId") final String ownerExternalId,
@QueryParam("offset") @Parameter(description = "offset") final Integer offset,
@QueryParam("limit") @Parameter(description = "limit") final Integer limit, @Context final UriInfo uriInfo) {
platformUserRightsContext.isAuthenticated();
return externalAssetOwnersReadService.retrieveJournalEntriesOfOwner(ownerExternalId, offset, limit);
}
private String getResult(Long loanId, String apiRequestBodyAsJson, String commandParam) {
final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson);
CommandWrapper commandRequest = null;
@@ -26,6 +26,7 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.cob.loan.LoanCOBBusinessStep;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.investor.config.InvestorModuleIsEnabledCondition;
import org.apache.fineract.investor.data.ExternalTransferStatus;
import org.apache.fineract.investor.data.ExternalTransferSubStatus;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer;
@@ -33,18 +34,22 @@ import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferDetails;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMapping;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMappingRepository;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferRepository;
import org.apache.fineract.investor.service.AccountingService;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.springframework.context.annotation.Conditional;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
@Slf4j
@Conditional(InvestorModuleIsEnabledCondition.class)
public class LoanAccountOwnerTransferBusinessStep implements LoanCOBBusinessStep {
public static final LocalDate FUTURE_DATE_9999_12_31 = LocalDate.of(9999, 12, 31);
private final ExternalAssetOwnerTransferRepository externalAssetOwnerTransferRepository;
private final ExternalAssetOwnerTransferLoanMappingRepository externalAssetOwnerTransferLoanMappingRepository;
private final AccountingService accountingService;
@Override
public Loan execute(Loan loan) {
@@ -109,7 +114,7 @@ public class LoanAccountOwnerTransferBusinessStep implements LoanCOBBusinessStep
externalAssetOwnerTransferRepository.save(activeExternalAssetOwnerTransfer);
buybackExternalAssetOwnerTransfer = externalAssetOwnerTransferRepository.save(buybackExternalAssetOwnerTransfer);
externalAssetOwnerTransferLoanMappingRepository.deleteByLoanIdAndOwnerTransfer(loan.getId(), activeExternalAssetOwnerTransfer);
// TODO: create asset ownership accounting entries
accountingService.createJournalEntriesForBuybackAssetTransfer(loan, buybackExternalAssetOwnerTransfer);
return buybackExternalAssetOwnerTransfer;
}
@@ -121,7 +126,7 @@ public class LoanAccountOwnerTransferBusinessStep implements LoanCOBBusinessStep
createActiveMapping(loan.getId(), newExternalAssetOwnerTransfer);
newExternalAssetOwnerTransfer
.setExternalAssetOwnerTransferDetails(createAssetOwnerTransferDetails(loan, newExternalAssetOwnerTransfer));
// TODO: create asset ownership accounting entries
accountingService.createJournalEntriesForSaleAssetTransfer(loan, newExternalAssetOwnerTransfer);
} else {
ExternalTransferSubStatus subStatus = ExternalTransferSubStatus.BALANCE_ZERO;
if (loan.getTotalOverpaid().compareTo(BigDecimal.ZERO) > 0) {
@@ -0,0 +1,30 @@
/**
* 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.config;
import org.apache.fineract.infrastructure.core.condition.PropertiesCondition;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
public class InvestorModuleIsEnabledCondition extends PropertiesCondition {
@Override
protected boolean matches(FineractProperties properties) {
return properties.getModule().getInvestor().isEnabled();
}
}
@@ -0,0 +1,30 @@
/**
* 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.data;
import lombok.Data;
import org.apache.fineract.accounting.journalentry.data.JournalEntryData;
import org.springframework.data.domain.Page;
@Data
public class ExternalOwnerJournalEntryData {
private ExternalTransferOwnerData ownerData;
private Page<JournalEntryData> journalEntryData;
}
@@ -0,0 +1,30 @@
/**
* 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.data;
import lombok.Data;
import org.apache.fineract.accounting.journalentry.data.JournalEntryData;
import org.springframework.data.domain.Page;
@Data
public class ExternalOwnerTransferJournalEntryData {
private ExternalTransferData transferData;
private Page<JournalEntryData> journalEntryData;
}
@@ -26,7 +26,7 @@ public class ExternalTransferData {
private Long transferId;
private ExternalTransferOwnerData owner;
private ExternalTransferLoanMappingData loan;
private ExternalTransferLoanData loan;
private ExternalTransferDataDetails details;
private String transferExternalId;
private String purchasePriceRatio;
@@ -21,7 +21,7 @@ package org.apache.fineract.investor.data;
import lombok.Data;
@Data
public class ExternalTransferLoanMappingData {
public class ExternalTransferLoanData {
private final Long loanId;
private final String externalId;
@@ -0,0 +1,47 @@
/**
* 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.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.accounting.journalentry.domain.JournalEntry;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
@Getter
@Setter
@Table(name = "m_external_asset_owner_journal_entry_mapping")
@NoArgsConstructor
@Entity
public class ExternalAssetOwnerJournalEntryMapping extends AbstractAuditableWithUTCDateTimeCustom {
@OneToOne
@JoinColumn(name = "journal_entry_id", nullable = false)
private JournalEntry journalEntry;
@ManyToOne
@JoinColumn(name = "owner_id", nullable = false)
private ExternalAssetOwner owner;
}
@@ -0,0 +1,35 @@
/**
* 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.domain;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface ExternalAssetOwnerJournalEntryMappingRepository extends JpaRepository<ExternalAssetOwnerJournalEntryMapping, Long>,
JpaSpecificationExecutor<ExternalAssetOwnerJournalEntryMapping> {
@Query("SELECT mapping FROM ExternalAssetOwnerJournalEntryMapping mapping WHERE mapping.owner.externalId = :ownerExternalId")
Page<ExternalAssetOwnerJournalEntryMapping> findByExternalOwnerId(@Param("ownerExternalId") ExternalId ownerExternalId,
PageRequest pageRequest);
}
@@ -0,0 +1,47 @@
/**
* 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.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.accounting.journalentry.domain.JournalEntry;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
@Getter
@Setter
@Table(name = "m_external_asset_owner_transfer_journal_entry_mapping")
@NoArgsConstructor
@Entity
public class ExternalAssetOwnerTransferJournalEntryMapping extends AbstractAuditableWithUTCDateTimeCustom {
@OneToOne
@JoinColumn(name = "journal_entry_id", nullable = false)
private JournalEntry journalEntry;
@ManyToOne
@JoinColumn(name = "owner_transfer_id", nullable = false)
private ExternalAssetOwnerTransfer ownerTransfer;
}
@@ -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.domain;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface ExternalAssetOwnerTransferJournalEntryMappingRepository
extends JpaRepository<ExternalAssetOwnerTransferJournalEntryMapping, Long>,
JpaSpecificationExecutor<ExternalAssetOwnerTransferJournalEntryMapping> {
@Query("SELECT mapping FROM ExternalAssetOwnerTransferJournalEntryMapping mapping WHERE mapping.ownerTransfer.id =:transferId")
Page<ExternalAssetOwnerTransferJournalEntryMapping> findByTransferId(@Param("transferId") Long transferId, PageRequest pageable);
}
@@ -18,11 +18,23 @@
*/
package org.apache.fineract.investor.domain;
import java.util.Optional;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface ExternalAssetOwnerTransferLoanMappingRepository extends JpaRepository<ExternalAssetOwnerTransferLoanMapping, Long>,
JpaSpecificationExecutor<ExternalAssetOwnerTransferLoanMapping> {
void deleteByLoanIdAndOwnerTransfer(Long loanId, ExternalAssetOwnerTransfer externalAssetOwnerTransfer);
Optional<ExternalAssetOwnerTransferLoanMapping> findByLoanId(Long loanId);
@Query("SELECT mapping FROM ExternalAssetOwnerTransferLoanMapping mapping WHERE mapping.ownerTransfer.externalLoanId =:externalLoanId")
Optional<ExternalAssetOwnerTransferLoanMapping> findByLoanExternalId(@Param("externalLoanId") ExternalId externalLoanId);
@Query("SELECT mapping FROM ExternalAssetOwnerTransferLoanMapping mapping WHERE mapping.ownerTransfer.externalId =:externalTransferId")
Optional<ExternalAssetOwnerTransferLoanMapping> findByTransferExternalId(@Param("externalTransferId") ExternalId externalTransferId);
}
@@ -0,0 +1,21 @@
/**
* 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.internal;
public class InternalAPIForTesting {}
@@ -0,0 +1,29 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.investor.service;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
public interface AccountingService {
void createJournalEntriesForSaleAssetTransfer(Loan loan, ExternalAssetOwnerTransfer transfer);
void createJournalEntriesForBuybackAssetTransfer(Loan loan, ExternalAssetOwnerTransfer transfer);
}
@@ -0,0 +1,197 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.investor.service;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.accounting.common.AccountingConstants;
import org.apache.fineract.accounting.financialactivityaccount.domain.FinancialActivityAccount;
import org.apache.fineract.accounting.financialactivityaccount.domain.FinancialActivityAccountRepositoryWrapper;
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
import org.apache.fineract.accounting.journalentry.domain.JournalEntry;
import org.apache.fineract.accounting.journalentry.domain.JournalEntryType;
import org.apache.fineract.investor.accounting.journalentry.service.InvestorAccountingHelper;
import org.apache.fineract.investor.domain.ExternalAssetOwnerJournalEntryMapping;
import org.apache.fineract.investor.domain.ExternalAssetOwnerJournalEntryMappingRepository;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferJournalEntryMapping;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferJournalEntryMappingRepository;
import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class AccountingServiceImpl implements AccountingService {
private final InvestorAccountingHelper helper;
private final ExternalAssetOwnerTransferJournalEntryMappingRepository externalAssetOwnerTransferJournalEntryMappingRepository;
private final ExternalAssetOwnerJournalEntryMappingRepository externalAssetOwnerJournalEntryMappingRepository;
private final FinancialActivityAccountRepositoryWrapper financialActivityAccountRepository;
private static boolean participateInTransfer(FinancialActivityAccount financialActivityAccount, JournalEntry journalEntry,
JournalEntryType filterType) {
return filterType.getValue().equals(journalEntry.getType())
&& !Objects.equals(financialActivityAccount.getGlAccount().getId(), journalEntry.getGlAccount().getId());
}
@Override
public void createJournalEntriesForSaleAssetTransfer(final Loan loan, final ExternalAssetOwnerTransfer transfer) {
List<JournalEntry> journalEntryList = createJournalEntries(loan, transfer, true);
createMappingToTransfer(transfer, journalEntryList);
createMappingToOwner(transfer, journalEntryList, JournalEntryType.DEBIT);
}
@Override
public void createJournalEntriesForBuybackAssetTransfer(final Loan loan, final ExternalAssetOwnerTransfer transfer) {
List<JournalEntry> journalEntryList = createJournalEntries(loan, transfer, false);
createMappingToTransfer(transfer, journalEntryList);
createMappingToOwner(transfer, journalEntryList, JournalEntryType.CREDIT);
}
@NotNull
private List<JournalEntry> createJournalEntries(Loan loan, ExternalAssetOwnerTransfer transfer, boolean isReversalOrder) {
this.helper.checkForBranchClosures(loan.getOffice().getId(), transfer.getSettlementDate());
// loan properties
final Long loanProductId = loan.getLoanProduct().getId();
final Long loanId = loan.getId();
final Office office = loan.getOffice();
final String currencyCode = loan.getCurrencyCode();
// transaction properties
final Long transactionId = transfer.getId();
final LocalDate transactionDate = transfer.getSettlementDate();
final BigDecimal principalAmount = loan.getLoanSummary().getTotalPrincipalOutstanding();
final BigDecimal interestAmount = loan.getLoanSummary().getTotalInterestOutstanding();
final BigDecimal feesAmount = loan.getLoanSummary().getTotalFeeChargesOutstanding();
final BigDecimal penaltiesAmount = loan.getLoanSummary().getTotalPenaltyChargesOutstanding();
final BigDecimal overPaymentAmount = loan.getTotalOverpaid();
// Moving money to asset transfer account
List<JournalEntry> journalEntryList = createJournalEntries(loanProductId, loanId, office, currencyCode, transactionId,
transactionDate, principalAmount, interestAmount, feesAmount, penaltiesAmount, overPaymentAmount, !isReversalOrder);
// Moving money from asset transfer account
journalEntryList.addAll(createJournalEntries(loanProductId, loanId, office, currencyCode, transactionId, transactionDate,
principalAmount, interestAmount, feesAmount, penaltiesAmount, overPaymentAmount, isReversalOrder));
return journalEntryList;
}
private void createMappingToOwner(ExternalAssetOwnerTransfer transfer, List<JournalEntry> journalEntryList,
JournalEntryType filterType) {
FinancialActivityAccount financialActivityAccount = this.financialActivityAccountRepository
.findByFinancialActivityTypeWithNotFoundDetection(AccountingConstants.FinancialActivity.ASSET_TRANSFER.getValue());
journalEntryList.forEach(journalEntry -> {
if (participateInTransfer(financialActivityAccount, journalEntry, filterType)) {
ExternalAssetOwnerJournalEntryMapping mapping = new ExternalAssetOwnerJournalEntryMapping();
mapping.setJournalEntry(journalEntry);
mapping.setOwner(transfer.getOwner());
externalAssetOwnerJournalEntryMappingRepository.saveAndFlush(mapping);
}
});
}
private void createMappingToTransfer(ExternalAssetOwnerTransfer transfer, List<JournalEntry> journalEntryList) {
journalEntryList.forEach(journalEntry -> {
ExternalAssetOwnerTransferJournalEntryMapping mapping = new ExternalAssetOwnerTransferJournalEntryMapping();
mapping.setJournalEntry(journalEntry);
mapping.setOwnerTransfer(transfer);
externalAssetOwnerTransferJournalEntryMappingRepository.saveAndFlush(mapping);
});
}
private List<JournalEntry> createJournalEntries(Long loanProductId, Long loanId, Office office, String currencyCode, Long transactionId,
LocalDate transactionDate, BigDecimal principalAmount, BigDecimal interestAmount, BigDecimal feesAmount,
BigDecimal penaltiesAmount, BigDecimal overPaymentAmount, boolean isReversalOrder) {
List<JournalEntry> journalEntryList = new ArrayList<>();
BigDecimal totalDebitAmount = BigDecimal.ZERO;
Map<GLAccount, BigDecimal> accountMap = new LinkedHashMap<>();
// principal entry
if (principalAmount != null && principalAmount.compareTo(BigDecimal.ZERO) > 0) {
totalDebitAmount = totalDebitAmount.add(principalAmount);
GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
AccountingConstants.AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue());
accountMap.put(account, principalAmount);
}
// interest entry
if (interestAmount != null && interestAmount.compareTo(BigDecimal.ZERO) > 0) {
totalDebitAmount = totalDebitAmount.add(interestAmount);
GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
AccountingConstants.AccrualAccountsForLoan.INTEREST_RECEIVABLE.getValue());
if (accountMap.containsKey(account)) {
BigDecimal amount = accountMap.get(account).add(interestAmount);
accountMap.put(account, amount);
} else {
accountMap.put(account, interestAmount);
}
}
// fee entry
if (feesAmount != null && feesAmount.compareTo(BigDecimal.ZERO) > 0) {
totalDebitAmount = totalDebitAmount.add(feesAmount);
GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
AccountingConstants.AccrualAccountsForLoan.FEES_RECEIVABLE.getValue());
if (accountMap.containsKey(account)) {
BigDecimal amount = accountMap.get(account).add(feesAmount);
accountMap.put(account, amount);
} else {
accountMap.put(account, feesAmount);
}
}
// penalty entry
if (penaltiesAmount != null && penaltiesAmount.compareTo(BigDecimal.ZERO) > 0) {
totalDebitAmount = totalDebitAmount.add(penaltiesAmount);
GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
AccountingConstants.AccrualAccountsForLoan.PENALTIES_RECEIVABLE.getValue());
if (accountMap.containsKey(account)) {
BigDecimal amount = accountMap.get(account).add(penaltiesAmount);
accountMap.put(account, amount);
} else {
accountMap.put(account, penaltiesAmount);
}
}
// overpaid entry
if (overPaymentAmount != null && overPaymentAmount.compareTo(BigDecimal.ZERO) > 0) {
totalDebitAmount = totalDebitAmount.add(overPaymentAmount);
GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
AccountingConstants.AccrualAccountsForLoan.OVERPAYMENT.getValue());
if (accountMap.containsKey(account)) {
BigDecimal amount = accountMap.get(account).add(overPaymentAmount);
accountMap.put(account, amount);
} else {
accountMap.put(account, overPaymentAmount);
}
}
// asset transfer entry
for (Map.Entry<GLAccount, BigDecimal> entry : accountMap.entrySet()) {
journalEntryList.add(this.helper.createCreditJournalEntryOrReversalForInvestor(office, currencyCode, loanId, transactionId,
transactionDate, entry.getValue(), isReversalOrder, entry.getKey()));
}
if (totalDebitAmount.compareTo(BigDecimal.ZERO) > 0) {
journalEntryList.add(this.helper.createDebitJournalEntryOrReversalForInvestor(office, currencyCode,
AccountingConstants.FinancialActivity.ASSET_TRANSFER.getValue(), loanProductId, loanId, transactionId, transactionDate,
totalDebitAmount, isReversalOrder));
}
return journalEntryList;
}
}
@@ -0,0 +1,21 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.investor.service;
public interface ExternalAssetOwnerJournalEntryService {}
@@ -0,0 +1,71 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.investor.service;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.accounting.journalentry.domain.JournalEntry;
import org.apache.fineract.infrastructure.event.business.BusinessEventListener;
import org.apache.fineract.infrastructure.event.business.domain.journalentry.LoanJournalEntryCreatedBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.investor.config.InvestorModuleIsEnabledCondition;
import org.apache.fineract.investor.domain.ExternalAssetOwnerJournalEntryMapping;
import org.apache.fineract.investor.domain.ExternalAssetOwnerJournalEntryMappingRepository;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMappingRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.springframework.context.annotation.Conditional;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
@Slf4j
@Conditional(InvestorModuleIsEnabledCondition.class)
public class ExternalAssetOwnerJournalEntryServiceImpl implements ExternalAssetOwnerJournalEntryService {
private final BusinessEventNotifierService businessEventNotifierService;
private final ExternalAssetOwnerJournalEntryMappingRepository externalAssetOwnerJournalEntryMappingRepository;
private final ExternalAssetOwnerTransferLoanMappingRepository externalAssetOwnerTransferLoanMappingRepository;
private final LoanTransactionRepository loanTransactionRepository;
@PostConstruct
public void addListeners() {
businessEventNotifierService.addPostBusinessEventListener(LoanJournalEntryCreatedBusinessEvent.class,
new HandleLoanJournalEntryCreatedBusinessEvent());
}
private class HandleLoanJournalEntryCreatedBusinessEvent implements BusinessEventListener<LoanJournalEntryCreatedBusinessEvent> {
@Override
public void onBusinessEvent(LoanJournalEntryCreatedBusinessEvent event) {
JournalEntry journalEntry = event.get();
createMapping(journalEntry);
}
private void createMapping(JournalEntry journalEntry) {
Long loanId = loanTransactionRepository.findLoanIdById(journalEntry.getLoanTransactionId()).orElseThrow();
externalAssetOwnerTransferLoanMappingRepository.findByLoanId(loanId).ifPresent(transferLoanMapping -> {
ExternalAssetOwnerJournalEntryMapping mapping = new ExternalAssetOwnerJournalEntryMapping();
mapping.setJournalEntry(journalEntry);
mapping.setOwner(transferLoanMapping.getOwnerTransfer().getOwner());
externalAssetOwnerJournalEntryMappingRepository.saveAndFlush(mapping);
});
}
}
}
@@ -18,6 +18,8 @@
*/
package org.apache.fineract.investor.service;
import org.apache.fineract.investor.data.ExternalOwnerJournalEntryData;
import org.apache.fineract.investor.data.ExternalOwnerTransferJournalEntryData;
import org.apache.fineract.investor.data.ExternalTransferData;
import org.springframework.data.domain.Page;
@@ -25,4 +27,10 @@ public interface ExternalAssetOwnersReadService {
Page<ExternalTransferData> retrieveTransferData(Long loanId, String externalLoanId, String transferExternalId, Integer offset,
Integer limit);
ExternalTransferData retrieveActiveTransferData(Long loanId, String externalLoanId, String transferExternalId);
ExternalOwnerTransferJournalEntryData retrieveJournalEntriesOfTransfer(Long transferId, Integer offset, Integer limit);
ExternalOwnerJournalEntryData retrieveJournalEntriesOfOwner(String ownerExternalId, Integer offset, Integer limit);
}
@@ -18,10 +18,20 @@
*/
package org.apache.fineract.investor.service;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.accounting.journalentry.JournalEntryMapper;
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
import org.apache.fineract.investor.data.ExternalOwnerJournalEntryData;
import org.apache.fineract.investor.data.ExternalOwnerTransferJournalEntryData;
import org.apache.fineract.investor.data.ExternalTransferData;
import org.apache.fineract.investor.domain.ExternalAssetOwnerJournalEntryMapping;
import org.apache.fineract.investor.domain.ExternalAssetOwnerJournalEntryMappingRepository;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferJournalEntryMapping;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferJournalEntryMappingRepository;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMapping;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMappingRepository;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
@@ -35,19 +45,17 @@ import org.springframework.transaction.annotation.Transactional;
public class ExternalAssetOwnersReadServiceImpl implements ExternalAssetOwnersReadService {
private final ExternalAssetOwnerTransferRepository externalAssetOwnerTransferRepository;
private final ExternalAssetOwnerTransferLoanMappingRepository externalAssetOwnerTransferLoanMappingRepository;
private final ExternalAssetOwnerTransferJournalEntryMappingRepository externalAssetOwnerTransferJournalEntryMappingRepository;
private final ExternalAssetOwnerJournalEntryMappingRepository externalAssetOwnerJournalEntryMappingRepository;
private final ExternalAssetOwnersTransferMapper mapper;
private final JournalEntryMapper journalEntryMapper;
@Override
public Page<ExternalTransferData> retrieveTransferData(Long loanId, String externalLoanId, String transferExternalId, Integer offset,
Integer limit) {
Page<ExternalAssetOwnerTransfer> result;
if (offset == null) {
offset = 0;
}
if (limit == null) {
limit = 100;
}
PageRequest pageRequest = PageRequest.of(offset, limit, Sort.by("id"));
PageRequest pageRequest = getPageRequest(offset, limit);
if (loanId != null) {
result = externalAssetOwnerTransferRepository.findAllByLoanId(loanId, pageRequest);
} else if (externalLoanId != null) {
@@ -61,4 +69,57 @@ public class ExternalAssetOwnersReadServiceImpl implements ExternalAssetOwnersRe
return result.map(mapper::mapTransfer);
}
@Override
public ExternalTransferData retrieveActiveTransferData(Long loanId, String externalLoanId, String transferExternalId) {
Optional<ExternalAssetOwnerTransferLoanMapping> result;
if (loanId != null) {
result = externalAssetOwnerTransferLoanMappingRepository.findByLoanId(loanId);
} else if (externalLoanId != null) {
result = externalAssetOwnerTransferLoanMappingRepository.findByLoanExternalId(ExternalIdFactory.produce(externalLoanId));
} else if (transferExternalId != null) {
result = externalAssetOwnerTransferLoanMappingRepository
.findByTransferExternalId(ExternalIdFactory.produce(transferExternalId));
} else {
throw new IllegalArgumentException(
"At least one of the following parameters must be provided: loanId, externalLoanId, transferExternalId");
}
return result.map(transferLoanMapping -> mapper.mapTransfer(transferLoanMapping.getOwnerTransfer())).orElse(null);
}
@Override
public ExternalOwnerTransferJournalEntryData retrieveJournalEntriesOfTransfer(Long transferId, Integer offset, Integer limit) {
PageRequest pageRequest = getPageRequest(offset, limit);
Page<ExternalAssetOwnerTransferJournalEntryMapping> result = externalAssetOwnerTransferJournalEntryMappingRepository
.findByTransferId(transferId, pageRequest);
ExternalOwnerTransferJournalEntryData mappedResult = new ExternalOwnerTransferJournalEntryData();
if (result.hasContent()) {
mappedResult.setJournalEntryData(result.map(entry -> journalEntryMapper.map(entry.getJournalEntry())));
mappedResult.setTransferData(mapper.mapTransfer(result.stream().findFirst().get().getOwnerTransfer()));
}
return mappedResult;
}
@Override
public ExternalOwnerJournalEntryData retrieveJournalEntriesOfOwner(String ownerExternalId, Integer offset, Integer limit) {
PageRequest pageRequest = getPageRequest(offset, limit);
Page<ExternalAssetOwnerJournalEntryMapping> result = externalAssetOwnerJournalEntryMappingRepository
.findByExternalOwnerId(ExternalIdFactory.produce(ownerExternalId), pageRequest);
ExternalOwnerJournalEntryData mappedResult = new ExternalOwnerJournalEntryData();
if (result.hasContent()) {
mappedResult.setJournalEntryData(result.map(entry -> journalEntryMapper.map(entry.getJournalEntry())));
mappedResult.setOwnerData(mapper.mapOwner(result.stream().findFirst().get().getOwner()));
}
return mappedResult;
}
private PageRequest getPageRequest(Integer offset, Integer limit) {
if (offset == null) {
offset = 0;
}
if (limit == null) {
limit = 100;
}
return PageRequest.of(offset, limit, Sort.by("id"));
}
}
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.investor.service;
import org.apache.fineract.accounting.journalentry.JournalEntryMapper;
import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig;
import org.apache.fineract.investor.data.ExternalTransferData;
import org.apache.fineract.investor.data.ExternalTransferDataDetails;
@@ -28,7 +29,7 @@ import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferDetails;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
@Mapper(config = MapstructMapperConfig.class)
@Mapper(config = MapstructMapperConfig.class, uses = JournalEntryMapper.class)
public interface ExternalAssetOwnersTransferMapper {
@Mapping(target = "transferId", source = "id")
@@ -51,7 +51,6 @@ import org.apache.fineract.investor.data.ExternalTransferStatus;
import org.apache.fineract.investor.domain.ExternalAssetOwner;
import org.apache.fineract.investor.domain.ExternalAssetOwnerRepository;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMappingRepository;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferRepository;
import org.apache.fineract.investor.exception.ExternalAssetOwnerInitiateTransferException;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
@@ -70,7 +69,6 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
private static final List<ExternalTransferStatus> BUYBACK_READY_STATUSES = List.of(ExternalTransferStatus.PENDING,
ExternalTransferStatus.ACTIVE);
private final ExternalAssetOwnerTransferRepository externalAssetOwnerTransferRepository;
private final ExternalAssetOwnerTransferLoanMappingRepository externalAssetOwnerTransferLoanMappingRepository;
private final ExternalAssetOwnerRepository externalAssetOwnerRepository;
private final FromJsonHelper fromApiJsonHelper;
private final LoanRepository loanRepository;
@@ -29,4 +29,5 @@
<include relativeToChangelogFile="true" file="parts/0005_add_sale_and_buyback_command.xml"/>
<include relativeToChangelogFile="true" file="parts/0006_asset_schemas.xml"/>
<include relativeToChangelogFile="true" file="parts/0007_add_external_asset_owner_transfer_details.xml"/>
<include relativeToChangelogFile="true" file="parts/0008_add_mappings.xml"/>
</databaseChangeLog>
@@ -0,0 +1,160 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!-- Create table m_external_asset_owner_transfer_journal_entry_mapping -->
<changeSet author="fineract" id="1" context="mysql">
<createTable tableName="m_external_asset_owner_transfer_journal_entry_mapping">
<column autoIncrement="true" name="id" type="BIGINT" remarks="Internal ID">
<constraints nullable="false" primaryKey="true"/>
</column>
<column name="journal_entry_id" type="bigint" remarks="Journal entry ID">
<constraints nullable="false"/>
</column>
<column name="owner_transfer_id" type="bigint" remarks="Owner transfer">
<constraints nullable="false"/>
</column>
<column name="created_by" type="bigint" remarks=""/>
<column name="created_on_utc" type="DATETIME" remarks=""/>
<column name="last_modified_by" type="bigint" remarks=""/>
<column name="last_modified_on_utc" type="DATETIME" remarks=""/>
</createTable>
</changeSet>
<changeSet author="fineract" id="1" context="postgresql">
<createTable tableName="m_external_asset_owner_transfer_journal_entry_mapping">
<column autoIncrement="true" name="id" type="BIGINT" remarks="Internal ID">
<constraints nullable="false" primaryKey="true"/>
</column>
<column name="journal_entry_id" type="bigint" remarks="Journal entry ID">
<constraints nullable="false"/>
</column>
<column name="owner_transfer_id" type="bigint" remarks="Owner transfer">
</column>
<column name="created_by" type="bigint" remarks=""/>
<column name="created_on_utc" type="TIMESTAMP WITH TIME ZONE" remarks=""/>
<column name="last_modified_by" type="bigint" remarks=""/>
<column name="last_modified_on_utc" type="TIMESTAMP WITH TIME ZONE" remarks=""/>
</createTable>
</changeSet>
<changeSet author="fineract" id="2">
<addForeignKeyConstraint baseColumnNames="created_by"
baseTableName="m_external_asset_owner_transfer_journal_entry_mapping"
constraintName="FK_ext_asset_owner_transfer_je_mapping_created_by" deferrable="false"
initiallyDeferred="false"
onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="id"
referencedTableName="m_appuser" validate="true"/>
<addForeignKeyConstraint baseColumnNames="last_modified_by"
baseTableName="m_external_asset_owner_transfer_journal_entry_mapping"
constraintName="FK_ext_asset_owner_transfer_je_mapping_modified_by" deferrable="false"
initiallyDeferred="false"
onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="id"
referencedTableName="m_appuser" validate="true"/>
<addForeignKeyConstraint baseColumnNames="journal_entry_id"
baseTableName="m_external_asset_owner_transfer_journal_entry_mapping"
constraintName="FK_ext_asset_owner_transfer_je_mapping_journal_entry_id" deferrable="false"
initiallyDeferred="false"
onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="id"
referencedTableName="acc_gl_journal_entry" validate="true"/>
<addForeignKeyConstraint baseColumnNames="owner_transfer_id"
baseTableName="m_external_asset_owner_transfer_journal_entry_mapping"
constraintName="FK_ext_asset_owner_transfer_je_mapping_owner_transfer_id"
deferrable="false" initiallyDeferred="false"
onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="id"
referencedTableName="m_external_asset_owner_transfer" validate="true"/>
</changeSet>
<!-- Create table m_external_asset_owner_journal_entry_mapping -->
<changeSet author="fineract" id="3" context="mysql">
<createTable tableName="m_external_asset_owner_journal_entry_mapping">
<column autoIncrement="true" name="id" type="BIGINT" remarks="Internal ID">
<constraints nullable="false" primaryKey="true"/>
</column>
<column name="journal_entry_id" type="bigint" remarks="Journal entry ID">
<constraints nullable="false"/>
</column>
<column name="owner_id" type="bigint" remarks="Owner">
<constraints nullable="false"/>
</column>
<column name="created_by" type="bigint" remarks=""/>
<column name="created_on_utc" type="DATETIME" remarks=""/>
<column name="last_modified_by" type="bigint" remarks=""/>
<column name="last_modified_on_utc" type="DATETIME" remarks=""/>
</createTable>
</changeSet>
<changeSet author="fineract" id="3" context="postgresql">
<createTable tableName="m_external_asset_owner_journal_entry_mapping">
<column autoIncrement="true" name="id" type="BIGINT" remarks="Internal ID">
<constraints nullable="false" primaryKey="true"/>
</column>
<column name="journal_entry_id" type="bigint" remarks="Journal entry ID">
<constraints nullable="false"/>
</column>
<column name="owner_id" type="bigint" remarks="Owner">
</column>
<column name="created_by" type="bigint" remarks=""/>
<column name="created_on_utc" type="TIMESTAMP WITH TIME ZONE" remarks=""/>
<column name="last_modified_by" type="bigint" remarks=""/>
<column name="last_modified_on_utc" type="TIMESTAMP WITH TIME ZONE" remarks=""/>
</createTable>
</changeSet>
<changeSet author="fineract" id="4">
<addForeignKeyConstraint baseColumnNames="created_by"
baseTableName="m_external_asset_owner_journal_entry_mapping"
constraintName="FK_ext_asset_owner_je_mapping_created_by" deferrable="false"
initiallyDeferred="false"
onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="id"
referencedTableName="m_appuser" validate="true"/>
<addForeignKeyConstraint baseColumnNames="last_modified_by"
baseTableName="m_external_asset_owner_journal_entry_mapping"
constraintName="FK_ext_asset_owner_je_mapping_modified_by" deferrable="false"
initiallyDeferred="false"
onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="id"
referencedTableName="m_appuser" validate="true"/>
<addForeignKeyConstraint baseColumnNames="journal_entry_id"
baseTableName="m_external_asset_owner_journal_entry_mapping"
constraintName="FK_ext_asset_owner_je_mapping_journal_entry_id" deferrable="false"
initiallyDeferred="false"
onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="id"
referencedTableName="acc_gl_journal_entry" validate="true"/>
<addForeignKeyConstraint baseColumnNames="owner_id"
baseTableName="m_external_asset_owner_journal_entry_mapping"
constraintName="FK_ext_asset_owner_je_mapping_owner_id"
deferrable="false" initiallyDeferred="false"
onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="id"
referencedTableName="m_external_asset_owner" validate="true"/>
</changeSet>
<!-- Add index to m_external_asset_owner_transfer_loan_mapping of loan_id -->
<changeSet id="5" author="fineract">
<createIndex tableName="m_external_asset_owner_transfer_loan_mapping"
indexName="IND_m_ext_asset_owner_transfer_loan_mapping_loan_id">
<column name="loan_id"/>
</createIndex>
</changeSet>
</databaseChangeLog>
@@ -30,6 +30,7 @@
<persistence-unit name="jpa-pu" transaction-type="RESOURCE_LOCAL">
<provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
<class>org.apache.fineract.accounting.glaccount.domain.GLAccount</class>
<class>org.apache.fineract.accounting.journalentry.domain.JournalEntry</class>
<class>org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom</class>
<class>org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom</class>
<class>org.apache.fineract.infrastructure.codes.domain.Code</class>
@@ -44,6 +44,7 @@ import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMapping;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMappingRepository;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferRepository;
import org.apache.fineract.investor.service.AccountingService;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanSummary;
import org.junit.jupiter.api.BeforeEach;
@@ -65,6 +66,8 @@ public class LoanAccountOwnerTransferBusinessStepTest {
private ExternalAssetOwnerTransferRepository externalAssetOwnerTransferRepository;
@Mock
private ExternalAssetOwnerTransferLoanMappingRepository externalAssetOwnerTransferLoanMappingRepository;
@Mock
private AccountingService accountingService;
private LoanAccountOwnerTransferBusinessStep underTest;
@BeforeEach
@@ -73,7 +76,7 @@ public class LoanAccountOwnerTransferBusinessStepTest {
ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
ThreadLocalContextUtil.setBusinessDates(new HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, actualDate)));
underTest = new LoanAccountOwnerTransferBusinessStep(externalAssetOwnerTransferRepository,
externalAssetOwnerTransferLoanMappingRepository);
externalAssetOwnerTransferLoanMappingRepository, accountingService);
}
@Test