FINERACT-2466: fix trial balance report
Signed-off-by: Attila Budai <budai.attila.istvan@gmail.com>
This commit is contained in:
@@ -229,4 +229,5 @@
|
||||
<include file="parts/0208_trial_balance_summary_with_asset_owner_journal_entry_aggregation_fix.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/0209_transaction_summary_with_asset_owner_and_from_asset_owner_id_for_asset_sales.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/0210_add_configuration_password_reuse_check_history_count.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/0211_trial_balance_summary_fix_external_owners_and_aggregation.xml" relativeToChangelogFile="true" />
|
||||
</databaseChangeLog>
|
||||
|
||||
+242
@@ -0,0 +1,242 @@
|
||||
<?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.3.xsd">
|
||||
|
||||
<changeSet author="fineract" id="trial-balance-summary-with-asset-owner-update-5">
|
||||
<update tableName="stretchy_report">
|
||||
<column name="report_sql">
|
||||
<![CDATA[
|
||||
WITH retained_earning AS (
|
||||
SELECT DISTINCT
|
||||
'${endDate}' AS postingdate,
|
||||
lp.name AS product,
|
||||
gl_code AS glacct,
|
||||
COALESCE((SELECT name FROM acc_gl_account WHERE gl_code = e.gl_code), '') AS description,
|
||||
COALESCE(e.owner_external_id, 'self') AS assetowner,
|
||||
SUM(opening_balance_amount) AS beginningbalance,
|
||||
0 AS debitmovement,
|
||||
0 AS creditmovement,
|
||||
SUM(opening_balance_amount) AS endingbalance
|
||||
FROM acc_gl_journal_entry_annual_summary e, m_product_loan lp
|
||||
WHERE e.office_id = ${officeId}
|
||||
AND lp.id = product_id
|
||||
AND EXTRACT(YEAR FROM e.year_end_date) < EXTRACT(YEAR FROM CAST('${endDate}' AS DATE))
|
||||
GROUP BY gl_code, lp.name, office_id, owner_external_id
|
||||
),
|
||||
aggregated_date AS (
|
||||
SELECT MAX(aggregated_on_date_to) AS latest
|
||||
FROM m_journal_entry_aggregation_tracking
|
||||
WHERE aggregated_on_date_to < '${endDate}'
|
||||
),
|
||||
summary_snapshot_baseline_data AS (
|
||||
SELECT
|
||||
lp.NAME AS productname,
|
||||
acc_gl_account.gl_code AS glcode,
|
||||
acc_gl_account.NAME AS glname,
|
||||
CASE WHEN ags.external_owner_id IS NULL THEN 0 ELSE ags.external_owner_id END AS assetowner,
|
||||
SUM(ags.debit_amount) AS debitamount,
|
||||
SUM(ags.credit_amount) AS creditamount
|
||||
FROM acc_gl_account
|
||||
JOIN m_journal_entry_aggregation_summary ags ON acc_gl_account.id = ags.gl_account_id
|
||||
JOIN m_product_loan lp ON lp.id = ags.product_id
|
||||
WHERE ags.entity_type_enum = 1
|
||||
AND ags.manual_entry = FALSE
|
||||
AND ags.aggregated_on_date <= (SELECT latest FROM aggregated_date)
|
||||
AND (ags.office_id = ${officeId})
|
||||
GROUP BY productname, glcode, glname, assetowner
|
||||
),
|
||||
post_snapshot_delta_data AS (
|
||||
SELECT
|
||||
lp.NAME AS productname,
|
||||
acc_gl_account.gl_code AS glcode,
|
||||
acc_gl_account.NAME AS glname,
|
||||
CASE WHEN aw.owner_id IS NULL THEN 0 ELSE aw.owner_id END AS assetowner,
|
||||
SUM(CASE WHEN acc_gl_journal_entry.type_enum = 2 THEN amount ELSE 0 END) AS debitamount,
|
||||
SUM(CASE WHEN acc_gl_journal_entry.type_enum = 1 THEN amount ELSE 0 END) AS creditamount
|
||||
FROM acc_gl_account
|
||||
JOIN acc_gl_journal_entry ON acc_gl_account.id = acc_gl_journal_entry.account_id
|
||||
JOIN m_loan m ON m.id = acc_gl_journal_entry.entity_id
|
||||
JOIN m_product_loan lp ON lp.id = m.product_id
|
||||
LEFT JOIN m_external_asset_owner_journal_entry_mapping aw ON aw.journal_entry_id = acc_gl_journal_entry.id
|
||||
WHERE acc_gl_journal_entry.entity_type_enum = 1
|
||||
AND acc_gl_journal_entry.manual_entry = FALSE
|
||||
AND (
|
||||
(SELECT latest FROM aggregated_date) IS NULL
|
||||
OR acc_gl_journal_entry.submitted_on_date > (SELECT latest FROM aggregated_date)
|
||||
)
|
||||
AND acc_gl_journal_entry.submitted_on_date < '${endDate}'
|
||||
AND (acc_gl_journal_entry.office_id = ${officeId})
|
||||
GROUP BY productname, glcode, glname, assetowner
|
||||
),
|
||||
merged_historical_data AS (
|
||||
SELECT
|
||||
COALESCE(s.productname, p.productname) AS productname,
|
||||
COALESCE(s.glcode, p.glcode) AS glcode,
|
||||
COALESCE(s.glname, p.glname) AS glname,
|
||||
COALESCE(s.assetowner, p.assetowner, 0) AS assetowner,
|
||||
COALESCE(s.debitamount, 0) + COALESCE(p.debitamount, 0) AS debitamount,
|
||||
COALESCE(s.creditamount, 0) + COALESCE(p.creditamount, 0) AS creditamount
|
||||
FROM summary_snapshot_baseline_data s
|
||||
LEFT JOIN post_snapshot_delta_data p
|
||||
ON s.glcode = p.glcode
|
||||
AND s.productname = p.productname
|
||||
AND s.assetowner = p.assetowner
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
p.productname AS productname,
|
||||
p.glcode AS glcode,
|
||||
p.glname AS glname,
|
||||
COALESCE(p.assetowner, 0) AS assetowner,
|
||||
COALESCE(p.debitamount, 0) AS debitamount,
|
||||
COALESCE(p.creditamount, 0) AS creditamount
|
||||
FROM post_snapshot_delta_data p
|
||||
LEFT JOIN summary_snapshot_baseline_data s
|
||||
ON s.glcode = p.glcode
|
||||
AND s.productname = p.productname
|
||||
AND s.assetowner = p.assetowner
|
||||
WHERE s.glcode IS NULL
|
||||
),
|
||||
current_cob_data AS (
|
||||
SELECT
|
||||
lp.name AS productname,
|
||||
account_id,
|
||||
acc_gl_account.gl_code AS glcode,
|
||||
acc_gl_account.name AS glname,
|
||||
CASE WHEN aw.owner_id IS NULL THEN 0 ELSE aw.owner_id END AS assetowner,
|
||||
SUM(CASE WHEN acc_gl_journal_entry.type_enum = 2 THEN amount ELSE 0 END) AS debitamount,
|
||||
SUM(CASE WHEN acc_gl_journal_entry.type_enum = 1 THEN amount ELSE 0 END) AS creditamount
|
||||
FROM acc_gl_journal_entry
|
||||
JOIN acc_gl_account ON acc_gl_account.id = acc_gl_journal_entry.account_id
|
||||
JOIN m_loan m ON m.id = acc_gl_journal_entry.entity_id
|
||||
JOIN m_product_loan lp ON lp.id = m.product_id
|
||||
LEFT JOIN m_external_asset_owner_journal_entry_mapping aw ON aw.journal_entry_id = acc_gl_journal_entry.id
|
||||
WHERE acc_gl_journal_entry.entity_type_enum = 1
|
||||
AND acc_gl_journal_entry.manual_entry = FALSE
|
||||
AND acc_gl_journal_entry.submitted_on_date = '${endDate}'
|
||||
AND (acc_gl_journal_entry.office_id = ${officeId})
|
||||
GROUP BY productname, account_id, glcode, glname, assetowner
|
||||
)
|
||||
|
||||
SELECT *
|
||||
FROM (
|
||||
SELECT *
|
||||
FROM retained_earning
|
||||
WHERE glacct = (SELECT gl_code FROM acc_gl_account WHERE name = 'Retained Earnings Prior Year')
|
||||
|
||||
UNION
|
||||
|
||||
SELECT
|
||||
txnreport.postingdate,
|
||||
txnreport.product,
|
||||
txnreport.glacct,
|
||||
txnreport.description,
|
||||
txnreport.assetowner,
|
||||
(COALESCE(txnreport.beginningbalance, 0) + COALESCE(summary.beginningbalance, 0)) AS beginningbalance,
|
||||
txnreport.debitmovement AS debitmovement,
|
||||
txnreport.creditmovement AS creditmovement,
|
||||
(COALESCE(txnreport.endingbalance, 0) + COALESCE(summary.beginningbalance, 0)) AS endingbalance
|
||||
FROM (
|
||||
SELECT *
|
||||
FROM (
|
||||
SELECT DISTINCT
|
||||
'${endDate}' AS postingdate,
|
||||
loan.pname AS product,
|
||||
loan.gl_code AS glacct,
|
||||
loan.glname AS description,
|
||||
COALESCE((SELECT external_id FROM m_external_asset_owner WHERE id = loan.assetowner), 'self') AS assetowner,
|
||||
loan.openingbalance AS beginningbalance,
|
||||
(loan.debitamount * 1) AS debitmovement,
|
||||
(loan.creditamount * -1) AS creditmovement,
|
||||
(loan.openingbalance + loan.debitamount - loan.creditamount) AS endingbalance
|
||||
FROM (
|
||||
SELECT DISTINCT
|
||||
g.pname AS pname,
|
||||
g.gl_code AS gl_code,
|
||||
g.glname AS glname,
|
||||
COALESCE(mh.assetowner, c.assetowner, 0) AS assetowner,
|
||||
COALESCE(mh.debitamount, 0) - COALESCE(mh.creditamount, 0) AS openingbalance,
|
||||
COALESCE(c.debitamount, 0) AS debitamount,
|
||||
COALESCE(c.creditamount, 0) AS creditamount
|
||||
FROM (
|
||||
SELECT DISTINCT ag.gl_code, ag.id, pl.NAME AS pname, ag.NAME AS glname
|
||||
FROM acc_gl_account ag
|
||||
JOIN acc_product_mapping am ON am.gl_account_id = ag.id AND am.product_type = 1
|
||||
JOIN m_product_loan pl ON pl.id = am.product_id
|
||||
) g
|
||||
LEFT JOIN merged_historical_data mh
|
||||
ON g.gl_code = mh.glcode
|
||||
AND mh.productname = g.pname
|
||||
LEFT JOIN current_cob_data c
|
||||
ON g.gl_code = c.glcode
|
||||
AND c.productname = g.pname
|
||||
AND mh.assetowner = c.assetowner
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT DISTINCT
|
||||
c.productname AS pname,
|
||||
c.glcode AS gl_code,
|
||||
c.glname AS glname,
|
||||
COALESCE(c.assetowner, 0) AS assetowner,
|
||||
0 AS openingbalance,
|
||||
COALESCE(c.debitamount, 0) AS debitamount,
|
||||
COALESCE(c.creditamount, 0) AS creditamount
|
||||
FROM current_cob_data c
|
||||
LEFT JOIN (
|
||||
SELECT g3.gl_code, g3.pname, mh.assetowner
|
||||
FROM (
|
||||
SELECT DISTINCT ag.gl_code, pl.NAME AS pname
|
||||
FROM acc_gl_account ag
|
||||
JOIN acc_product_mapping am ON am.gl_account_id = ag.id AND am.product_type = 1
|
||||
JOIN m_product_loan pl ON pl.id = am.product_id
|
||||
) g3
|
||||
LEFT JOIN merged_historical_data mh
|
||||
ON g3.gl_code = mh.glcode
|
||||
AND mh.productname = g3.pname
|
||||
) matched
|
||||
ON matched.gl_code = c.glcode
|
||||
AND matched.pname = c.productname
|
||||
AND matched.assetowner = c.assetowner
|
||||
WHERE matched.gl_code IS NULL
|
||||
) loan
|
||||
) a
|
||||
) AS txnreport
|
||||
LEFT JOIN retained_earning summary
|
||||
ON txnreport.glacct = summary.glacct
|
||||
AND txnreport.assetowner = summary.assetowner
|
||||
AND summary.product = txnreport.product
|
||||
) report
|
||||
WHERE report.endingbalance != 0
|
||||
OR report.debitmovement != 0
|
||||
OR report.creditmovement != 0
|
||||
ORDER BY glacct
|
||||
|
||||
]]>
|
||||
</column>
|
||||
<where>report_name='Trial Balance Summary Report with Asset Owner'</where>
|
||||
</update>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
+6
@@ -71,6 +71,12 @@ public class FeignAccountHelper {
|
||||
return new Account(account.getId().intValue(), getAccountType(type));
|
||||
}
|
||||
|
||||
public String getGlCode(Account account) {
|
||||
GetGLAccountsResponse response = ok(
|
||||
() -> fineractClient.generalLedgerAccount().retreiveAccount(account.getAccountID().longValue(), Collections.emptyMap()));
|
||||
return response.getGlCode();
|
||||
}
|
||||
|
||||
private Integer getAccountTypeId(String type) {
|
||||
return switch (type) {
|
||||
case "ASSET" -> 1;
|
||||
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* 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.integrationtests.client.feign.helpers;
|
||||
|
||||
import static org.apache.fineract.client.feign.util.FeignCalls.ok;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.List;
|
||||
import org.apache.fineract.client.feign.FineractFeignClient;
|
||||
import org.apache.fineract.client.feign.util.FeignCalls;
|
||||
import org.apache.fineract.client.models.ExecuteJobRequest;
|
||||
import org.apache.fineract.client.models.GetJobsResponse;
|
||||
import org.apache.fineract.client.models.JobDetailHistoryData;
|
||||
import org.awaitility.Awaitility;
|
||||
|
||||
public class FeignSchedulerHelper {
|
||||
|
||||
private final FineractFeignClient fineractClient;
|
||||
|
||||
public FeignSchedulerHelper(FineractFeignClient fineractClient) {
|
||||
this.fineractClient = fineractClient;
|
||||
}
|
||||
|
||||
public void stopScheduler() {
|
||||
FeignCalls.executeVoid(() -> fineractClient.scheduler().changeSchedulerStatus("stop"));
|
||||
}
|
||||
|
||||
public void startScheduler() {
|
||||
FeignCalls.executeVoid(() -> fineractClient.scheduler().changeSchedulerStatus("start"));
|
||||
}
|
||||
|
||||
public void executeAndAwaitJob(String jobDisplayName) {
|
||||
stopScheduler();
|
||||
|
||||
List<GetJobsResponse> allJobs = ok(() -> fineractClient.schedulerJob().retrieveAll8());
|
||||
GetJobsResponse targetJob = allJobs.stream().filter(j -> jobDisplayName.equals(j.getDisplayName())).findFirst()
|
||||
.orElseThrow(() -> new RuntimeException("Job not found: " + jobDisplayName));
|
||||
|
||||
Instant beforeExecuteTime = Instant.now().truncatedTo(ChronoUnit.SECONDS);
|
||||
FeignCalls.executeVoid(() -> fineractClient.schedulerJob().executeJob(targetJob.getJobId(), "executeJob", new ExecuteJobRequest()));
|
||||
|
||||
Awaitility.await().atMost(Duration.ofMinutes(2)).pollInterval(Duration.ofSeconds(1)).pollDelay(Duration.ofSeconds(1)).until(() -> {
|
||||
GetJobsResponse job = ok(() -> fineractClient.schedulerJob().retrieveOne5(targetJob.getJobId()));
|
||||
JobDetailHistoryData history = job.getLastRunHistory();
|
||||
if (history == null || history.getJobRunStartTime() == null) {
|
||||
return false;
|
||||
}
|
||||
OffsetDateTime startTime = history.getJobRunStartTime();
|
||||
if (startTime.toInstant().isBefore(beforeExecuteTime)) {
|
||||
return false;
|
||||
}
|
||||
return history.getJobRunEndTime() != null && !history.getJobRunEndTime().toInstant().isBefore(startTime.toInstant());
|
||||
});
|
||||
}
|
||||
}
|
||||
+427
@@ -0,0 +1,427 @@
|
||||
/**
|
||||
* 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.integrationtests.client.feign.tests;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.client.models.BusinessStep;
|
||||
import org.apache.fineract.client.models.BusinessStepRequest;
|
||||
import org.apache.fineract.client.models.ChargeRequest;
|
||||
import org.apache.fineract.client.models.ExternalAssetOwnerRequest;
|
||||
import org.apache.fineract.client.models.GetFinancialActivityAccountsResponse;
|
||||
import org.apache.fineract.client.models.PostClientsRequest;
|
||||
import org.apache.fineract.client.models.PostFinancialActivityAccountsRequest;
|
||||
import org.apache.fineract.client.models.PostInitiateTransferResponse;
|
||||
import org.apache.fineract.client.models.PostLoanProductsRequest;
|
||||
import org.apache.fineract.client.models.PostLoansLoanIdChargesRequest;
|
||||
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.ResultsetColumnHeaderData;
|
||||
import org.apache.fineract.client.models.ResultsetRowData;
|
||||
import org.apache.fineract.client.models.RunReportsResponse;
|
||||
import org.apache.fineract.integrationtests.client.FeignIntegrationTest;
|
||||
import org.apache.fineract.integrationtests.client.feign.helpers.FeignAccountHelper;
|
||||
import org.apache.fineract.integrationtests.client.feign.helpers.FeignBusinessDateHelper;
|
||||
import org.apache.fineract.integrationtests.client.feign.helpers.FeignClientHelper;
|
||||
import org.apache.fineract.integrationtests.client.feign.helpers.FeignGlobalConfigurationHelper;
|
||||
import org.apache.fineract.integrationtests.client.feign.helpers.FeignLoanHelper;
|
||||
import org.apache.fineract.integrationtests.client.feign.helpers.FeignSchedulerHelper;
|
||||
import org.apache.fineract.integrationtests.client.feign.helpers.FeignTransactionHelper;
|
||||
import org.apache.fineract.integrationtests.client.feign.modules.LoanTestData;
|
||||
import org.apache.fineract.integrationtests.common.Utils;
|
||||
import org.apache.fineract.integrationtests.common.accounting.Account;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.MethodOrderer;
|
||||
import org.junit.jupiter.api.Order;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
|
||||
@Slf4j
|
||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||
public class FeignTrialBalanceSummaryReportTest extends FeignIntegrationTest {
|
||||
|
||||
private static final String REPORT_NAME = "Trial Balance Summary Report with Asset Owner";
|
||||
private static final BigDecimal LOAN_PRINCIPAL = BigDecimal.valueOf(15000.0);
|
||||
|
||||
private FeignAccountHelper accountHelper;
|
||||
private FeignGlobalConfigurationHelper globalConfigHelper;
|
||||
private FeignBusinessDateHelper businessDateHelper;
|
||||
private FeignLoanHelper loanHelper;
|
||||
private FeignTransactionHelper transactionHelper;
|
||||
private FeignClientHelper clientHelper;
|
||||
private FeignSchedulerHelper schedulerHelper;
|
||||
|
||||
private Account assetAccount;
|
||||
private Account feePenaltyAccount;
|
||||
private Account transferAccount;
|
||||
private Account expenseAccount;
|
||||
private Account incomeAccount;
|
||||
private Account overpaymentAccount;
|
||||
private String todaysDate;
|
||||
private List<BusinessStep> originalBusinessSteps;
|
||||
private List<GetFinancialActivityAccountsResponse> originalFinancialMappings;
|
||||
|
||||
@BeforeAll
|
||||
public void setup() {
|
||||
accountHelper = new FeignAccountHelper(fineractClient());
|
||||
globalConfigHelper = new FeignGlobalConfigurationHelper(fineractClient());
|
||||
businessDateHelper = new FeignBusinessDateHelper(fineractClient());
|
||||
loanHelper = new FeignLoanHelper(fineractClient());
|
||||
transactionHelper = new FeignTransactionHelper(fineractClient());
|
||||
clientHelper = new FeignClientHelper(fineractClient());
|
||||
schedulerHelper = new FeignSchedulerHelper(fineractClient());
|
||||
|
||||
todaysDate = Utils.getLocalDateOfTenant().toString();
|
||||
|
||||
originalBusinessSteps = ok(
|
||||
() -> fineractClient().businessStepConfiguration().retrieveAllConfiguredBusinessStep("LOAN_CLOSE_OF_BUSINESS"))
|
||||
.getBusinessSteps();
|
||||
originalFinancialMappings = ok(() -> fineractClient().mappingFinancialActivitiesToAccounts().retrieveAll());
|
||||
|
||||
configureBusinessSteps();
|
||||
|
||||
assetAccount = accountHelper.createAssetAccount("TrialBal");
|
||||
feePenaltyAccount = accountHelper.createAssetAccount("TrialBalFP");
|
||||
transferAccount = accountHelper.createAssetAccount("TrialBalTR");
|
||||
expenseAccount = accountHelper.createExpenseAccount("TrialBalEXP");
|
||||
incomeAccount = accountHelper.createIncomeAccount("TrialBalINC");
|
||||
overpaymentAccount = accountHelper.createLiabilityAccount("TrialBalOP");
|
||||
|
||||
setupFinancialActivityMapping();
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public void tearDown() {
|
||||
executeVoid(() -> fineractClient().businessStepConfiguration().updateJobBusinessStepConfig("LOAN_CLOSE_OF_BUSINESS",
|
||||
new BusinessStepRequest().businessSteps(originalBusinessSteps)));
|
||||
List<GetFinancialActivityAccountsResponse> currentMappings = ok(
|
||||
() -> fineractClient().mappingFinancialActivitiesToAccounts().retrieveAll());
|
||||
for (GetFinancialActivityAccountsResponse mapping : currentMappings) {
|
||||
executeVoid(() -> fineractClient().mappingFinancialActivitiesToAccounts().deleteGLAccount(mapping.getId()));
|
||||
}
|
||||
for (GetFinancialActivityAccountsResponse mapping : originalFinancialMappings) {
|
||||
ok(() -> fineractClient().mappingFinancialActivitiesToAccounts()
|
||||
.createGLAccount(new PostFinancialActivityAccountsRequest()
|
||||
.financialActivityId(Long.valueOf(mapping.getFinancialActivityData().getId()))
|
||||
.glAccountId(mapping.getGlAccountData().getId())));
|
||||
}
|
||||
schedulerHelper.startScheduler();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(1)
|
||||
public void testReportReturnsExpectedColumns() {
|
||||
RunReportsResponse response = runReport("2020-01-01");
|
||||
|
||||
assertNotNull(response);
|
||||
assertNotNull(response.getColumnHeaders());
|
||||
assertFalse(response.getColumnHeaders().isEmpty());
|
||||
|
||||
List<String> expectedColumns = List.of("postingdate", "product", "glacct", "description", "assetowner", "beginningbalance",
|
||||
"debitmovement", "creditmovement", "endingbalance");
|
||||
List<String> actualColumns = response.getColumnHeaders().stream().map(ResultsetColumnHeaderData::getColumnName).toList();
|
||||
Assertions.assertEquals(expectedColumns, actualColumns);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(2)
|
||||
public void testExternalAssetOwnerEntriesAppearInReport() {
|
||||
runWithBusinessDate("2020-03-02", () -> {
|
||||
Long clientId = createClient("01 March 2020");
|
||||
Long loanId = createAndDisburseLoan(clientId, "01 March 2020", "02 March 2020", null);
|
||||
|
||||
String ownerExternalId = UUID.randomUUID().toString();
|
||||
PostInitiateTransferResponse saleResponse = ok(() -> fineractClient().externalAssetOwners().transferRequestWithLoanId(loanId,
|
||||
new ExternalAssetOwnerRequest().settlementDate("2020-03-02").dateFormat("yyyy-MM-dd").locale(LoanTestData.LOCALE)
|
||||
.transferExternalId(UUID.randomUUID().toString()).ownerExternalId(ownerExternalId).purchasePriceRatio("1.0"),
|
||||
"sale"));
|
||||
assertNotNull(saleResponse);
|
||||
|
||||
advanceBusinessDateAndRunCob("2020-03-03");
|
||||
|
||||
RunReportsResponse report = runReport("2020-03-02");
|
||||
assertNotNull(report.getData());
|
||||
|
||||
int assetOwnerColIdx = findColumnIndex(report, "assetowner");
|
||||
boolean hasExternalOwnerEntry = report.getData().stream()
|
||||
.anyMatch(row -> ownerExternalId.equals(String.valueOf(row.getRow().get(assetOwnerColIdx))));
|
||||
|
||||
assertTrue(hasExternalOwnerEntry,
|
||||
"Report must contain entries for external asset owner '" + ownerExternalId + "'. Actual rows: " + report.getData());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(3)
|
||||
public void testBalanceFormulaIsConsistent() {
|
||||
runWithBusinessDate("2020-06-01", () -> {
|
||||
Long clientId = createClient("01 June 2020");
|
||||
Long chargeId = createFlatFeeCharge(500.0);
|
||||
Long loanId = createAndDisburseLoan(clientId, "01 June 2020", "01 June 2020", chargeId);
|
||||
|
||||
String productName = loanHelper.getLoanDetails(loanId).getLoanProductName();
|
||||
|
||||
transactionHelper.addRepayment(loanId, new PostLoansLoanIdTransactionsRequest().transactionDate("01 June 2020")
|
||||
.transactionAmount(1500.0).locale(LoanTestData.LOCALE).dateFormat(LoanTestData.DATETIME_PATTERN));
|
||||
|
||||
RunReportsResponse report = runReport("2020-06-01");
|
||||
assertNotNull(report.getData());
|
||||
|
||||
int productIdx = findColumnIndex(report, "product");
|
||||
int assetOwnerIdx = findColumnIndex(report, "assetowner");
|
||||
int beginBalIdx = findColumnIndex(report, "beginningbalance");
|
||||
int debitIdx = findColumnIndex(report, "debitmovement");
|
||||
int creditIdx = findColumnIndex(report, "creditmovement");
|
||||
int endingBalIdx = findColumnIndex(report, "endingbalance");
|
||||
int glAcctIdx = findColumnIndex(report, "glacct");
|
||||
|
||||
List<ResultsetRowData> productRows = report.getData().stream()
|
||||
.filter(row -> productName.equals(String.valueOf(row.getRow().get(productIdx)))).toList();
|
||||
|
||||
assertFalse(productRows.isEmpty(), "Report must contain entries for product '" + productName + "'.");
|
||||
|
||||
boolean allSelfOwned = productRows.stream().allMatch(row -> "self".equals(String.valueOf(row.getRow().get(assetOwnerIdx))));
|
||||
assertTrue(allSelfOwned, "All entries should be 'self' when no external asset owner transfer exists.");
|
||||
|
||||
assertTrue(productRows.size() >= 2, "Report must contain at least 2 GL account rows for a product with charges.");
|
||||
|
||||
String assetGlCode = accountHelper.getGlCode(assetAccount);
|
||||
String feeGlCode = accountHelper.getGlCode(feePenaltyAccount);
|
||||
boolean hasAssetRow = productRows.stream().anyMatch(row -> assetGlCode.equals(String.valueOf(row.getRow().get(glAcctIdx))));
|
||||
boolean hasFeeRow = productRows.stream().anyMatch(row -> feeGlCode.equals(String.valueOf(row.getRow().get(glAcctIdx))));
|
||||
assertTrue(hasAssetRow, "Report must contain asset account row.");
|
||||
assertTrue(hasFeeRow, "Report must contain fee/penalty receivable row.");
|
||||
|
||||
for (ResultsetRowData row : productRows) {
|
||||
BigDecimal beginBal = parseBigDecimal(row.getRow().get(beginBalIdx));
|
||||
BigDecimal debit = parseBigDecimal(row.getRow().get(debitIdx));
|
||||
BigDecimal credit = parseBigDecimal(row.getRow().get(creditIdx));
|
||||
BigDecimal endBal = parseBigDecimal(row.getRow().get(endingBalIdx));
|
||||
|
||||
Assertions.assertEquals(0, BigDecimal.ZERO.compareTo(beginBal),
|
||||
"Beginning balance should be 0 when no prior year data exists. Row: " + row.getRow());
|
||||
|
||||
BigDecimal expectedEndBal = beginBal.add(debit).add(credit);
|
||||
Assertions.assertEquals(0, expectedEndBal.compareTo(endBal),
|
||||
"Ending balance must equal beginning + debit + credit. Expected: " + expectedEndBal + ", Actual: " + endBal);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(4)
|
||||
public void testNoRetainedEarningsWithoutAnnualSummary() {
|
||||
RunReportsResponse report = runReport("2020-06-01");
|
||||
assertNotNull(report);
|
||||
assertNotNull(report.getData());
|
||||
|
||||
int glAcctIdx = findColumnIndex(report, "glacct");
|
||||
boolean hasRetainedEarningsRow = report.getData().stream()
|
||||
.anyMatch(row -> "320000".equals(String.valueOf(row.getRow().get(glAcctIdx))));
|
||||
assertFalse(hasRetainedEarningsRow, "Report should not contain Retained Earnings rows when no annual summary data exists.");
|
||||
}
|
||||
|
||||
private void configureBusinessSteps() {
|
||||
List<String> stepNames = List.of("APPLY_CHARGE_TO_OVERDUE_LOANS", "LOAN_DELINQUENCY_CLASSIFICATION", "CHECK_LOAN_REPAYMENT_DUE",
|
||||
"CHECK_LOAN_REPAYMENT_OVERDUE", "UPDATE_LOAN_ARREARS_AGING", "ADD_PERIODIC_ACCRUAL_ENTRIES",
|
||||
"EXTERNAL_ASSET_OWNER_TRANSFER");
|
||||
List<BusinessStep> steps = IntStream.range(0, stepNames.size())
|
||||
.mapToObj(i -> new BusinessStep().stepName(stepNames.get(i)).order((long) (i + 1))).collect(Collectors.toList());
|
||||
executeVoid(() -> fineractClient().businessStepConfiguration().updateJobBusinessStepConfig("LOAN_CLOSE_OF_BUSINESS",
|
||||
new BusinessStepRequest().businessSteps(steps)));
|
||||
}
|
||||
|
||||
private void setupFinancialActivityMapping() {
|
||||
List<GetFinancialActivityAccountsResponse> mappings = ok(
|
||||
() -> fineractClient().mappingFinancialActivitiesToAccounts().retrieveAll());
|
||||
for (GetFinancialActivityAccountsResponse mapping : mappings) {
|
||||
executeVoid(() -> fineractClient().mappingFinancialActivitiesToAccounts().deleteGLAccount(mapping.getId()));
|
||||
}
|
||||
ok(() -> fineractClient().mappingFinancialActivitiesToAccounts().createGLAccount(
|
||||
new PostFinancialActivityAccountsRequest().financialActivityId(100L).glAccountId((long) transferAccount.getAccountID())));
|
||||
}
|
||||
|
||||
private void runWithBusinessDate(String date, Runnable action) {
|
||||
try {
|
||||
globalConfigHelper.updateConfigurationByName("enable-business-date", true);
|
||||
globalConfigHelper.updateConfigurationByName("enable-auto-generated-external-id", true);
|
||||
businessDateHelper.updateBusinessDate("BUSINESS_DATE", date);
|
||||
action.run();
|
||||
} finally {
|
||||
businessDateHelper.updateBusinessDate("BUSINESS_DATE", todaysDate);
|
||||
globalConfigHelper.updateConfigurationByName("enable-business-date", false);
|
||||
globalConfigHelper.updateConfigurationByName("enable-auto-generated-external-id", false);
|
||||
}
|
||||
}
|
||||
|
||||
private void advanceBusinessDateAndRunCob(String date) {
|
||||
businessDateHelper.updateBusinessDate("BUSINESS_DATE", date);
|
||||
schedulerHelper.executeAndAwaitJob("Loan COB");
|
||||
}
|
||||
|
||||
private RunReportsResponse runReport(String endDate) {
|
||||
return ok(() -> fineractClient().runReports().runReportGetData(REPORT_NAME, Map.of("R_endDate", endDate, "R_officeId", "1")));
|
||||
}
|
||||
|
||||
private int findColumnIndex(RunReportsResponse report, String columnName) {
|
||||
List<ResultsetColumnHeaderData> headers = report.getColumnHeaders();
|
||||
for (int i = 0; i < headers.size(); i++) {
|
||||
if (columnName.equals(headers.get(i).getColumnName())) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Column '" + columnName + "' not found. Available: "
|
||||
+ headers.stream().map(ResultsetColumnHeaderData::getColumnName).toList());
|
||||
}
|
||||
|
||||
private BigDecimal parseBigDecimal(Object value) {
|
||||
if (value == null) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
String str = String.valueOf(value);
|
||||
if (str.isEmpty() || "null".equalsIgnoreCase(str)) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
return new BigDecimal(str);
|
||||
}
|
||||
|
||||
private Long createClient(String activationDate) {
|
||||
return clientHelper.createClient(new PostClientsRequest()//
|
||||
.officeId(1L)//
|
||||
.legalFormId(1L)//
|
||||
.firstname(Utils.randomFirstNameGenerator())//
|
||||
.lastname(Utils.randomLastNameGenerator())//
|
||||
.externalId(Utils.randomStringGenerator("EXT_", 7))//
|
||||
.active(true)//
|
||||
.activationDate(activationDate)//
|
||||
.dateFormat(LoanTestData.DATETIME_PATTERN)//
|
||||
.locale(LoanTestData.LOCALE));
|
||||
}
|
||||
|
||||
private Long createAndDisburseLoan(Long clientId, String submitDate, String disburseDate, Long chargeId) {
|
||||
Long loanProductId = createLoanProduct();
|
||||
assertNotNull(loanProductId);
|
||||
|
||||
PostLoansRequest loanRequest = new PostLoansRequest()//
|
||||
.clientId(clientId)//
|
||||
.productId(loanProductId)//
|
||||
.loanType("individual")//
|
||||
.submittedOnDate(submitDate)//
|
||||
.expectedDisbursementDate(submitDate)//
|
||||
.principal(LOAN_PRINCIPAL)//
|
||||
.loanTermFrequency(4)//
|
||||
.loanTermFrequencyType(2)//
|
||||
.numberOfRepayments(4)//
|
||||
.repaymentEvery(1)//
|
||||
.repaymentFrequencyType(2)//
|
||||
.interestRatePerPeriod(BigDecimal.valueOf(2.0))//
|
||||
.amortizationType(1)//
|
||||
.interestType(0)//
|
||||
.interestCalculationPeriodType(1)//
|
||||
.transactionProcessingStrategyCode("mifos-standard-strategy")//
|
||||
.locale(LoanTestData.LOCALE)//
|
||||
.dateFormat(LoanTestData.DATETIME_PATTERN);
|
||||
|
||||
Long loanId = loanHelper.applyForLoan(loanRequest);
|
||||
assertNotNull(loanId);
|
||||
|
||||
if (chargeId != null) {
|
||||
ok(() -> fineractClient().loanCharges().executeLoanCharge(loanId, new PostLoansLoanIdChargesRequest().chargeId(chargeId)
|
||||
.amount(500.0).dueDate(disburseDate).dateFormat(LoanTestData.DATETIME_PATTERN).locale(LoanTestData.LOCALE),
|
||||
(String) null));
|
||||
}
|
||||
|
||||
loanHelper.approveLoan(loanId, new PostLoansLoanIdRequest()//
|
||||
.approvedLoanAmount(LOAN_PRINCIPAL)//
|
||||
.approvedOnDate(submitDate)//
|
||||
.locale(LoanTestData.LOCALE)//
|
||||
.dateFormat(LoanTestData.DATETIME_PATTERN));
|
||||
|
||||
var loanDetails = loanHelper.getLoanDetails(loanId);
|
||||
loanHelper.disburseLoan(loanId, new PostLoansLoanIdRequest()//
|
||||
.actualDisbursementDate(disburseDate)//
|
||||
.transactionAmount(loanDetails.getNetDisbursalAmount())//
|
||||
.locale(LoanTestData.LOCALE)//
|
||||
.dateFormat(LoanTestData.DATETIME_PATTERN));
|
||||
|
||||
return loanId;
|
||||
}
|
||||
|
||||
private Long createFlatFeeCharge(double amount) {
|
||||
return ok(() -> fineractClient().charges().createCharge(new ChargeRequest()//
|
||||
.name("TB Fee " + System.currentTimeMillis())//
|
||||
.currencyCode("USD")//
|
||||
.chargeAppliesTo(1)//
|
||||
.chargeTimeType(2)//
|
||||
.chargeCalculationType(1)//
|
||||
.chargePaymentMode(0)//
|
||||
.amount(amount)//
|
||||
.active(true)//
|
||||
.locale(LoanTestData.LOCALE))).getResourceId();
|
||||
}
|
||||
|
||||
private Long createLoanProduct() {
|
||||
return loanHelper.createLoanProduct(new PostLoanProductsRequest()//
|
||||
.name("TrialBal Product " + System.currentTimeMillis())//
|
||||
.shortName(UUID.randomUUID().toString().substring(0, 4).toUpperCase())//
|
||||
.currencyCode("USD")//
|
||||
.digitsAfterDecimal(2)//
|
||||
.inMultiplesOf(1)//
|
||||
.principal(LOAN_PRINCIPAL.doubleValue())//
|
||||
.numberOfRepayments(4)//
|
||||
.repaymentEvery(1)//
|
||||
.repaymentFrequencyType(2L)//
|
||||
.interestRatePerPeriod(1.0)//
|
||||
.interestRateFrequencyType(2)//
|
||||
.amortizationType(1)//
|
||||
.interestType(0)//
|
||||
.interestCalculationPeriodType(1)//
|
||||
.transactionProcessingStrategyCode("mifos-standard-strategy")//
|
||||
.daysInYearType(365)//
|
||||
.daysInMonthType(30)//
|
||||
.isInterestRecalculationEnabled(false)//
|
||||
.accountingRule(3)//
|
||||
.loanPortfolioAccountId((long) assetAccount.getAccountID())//
|
||||
.transfersInSuspenseAccountId((long) assetAccount.getAccountID())//
|
||||
.interestOnLoanAccountId((long) incomeAccount.getAccountID())//
|
||||
.incomeFromFeeAccountId((long) incomeAccount.getAccountID())//
|
||||
.incomeFromPenaltyAccountId((long) incomeAccount.getAccountID())//
|
||||
.writeOffAccountId((long) expenseAccount.getAccountID())//
|
||||
.overpaymentLiabilityAccountId((long) overpaymentAccount.getAccountID())//
|
||||
.receivableInterestAccountId((long) feePenaltyAccount.getAccountID())//
|
||||
.receivableFeeAccountId((long) feePenaltyAccount.getAccountID())//
|
||||
.receivablePenaltyAccountId((long) feePenaltyAccount.getAccountID())//
|
||||
.fundSourceAccountId((long) assetAccount.getAccountID())//
|
||||
.incomeFromRecoveryAccountId((long) incomeAccount.getAccountID())//
|
||||
.locale(LoanTestData.LOCALE)//
|
||||
.dateFormat(LoanTestData.DATETIME_PATTERN));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user