Skip to content

Commit dbe72e1

Browse files
FINERACT-1289: Taxes on Loan charges
1 parent 4dd3b6b commit dbe72e1

30 files changed

Lines changed: 1745 additions & 85 deletions

File tree

fineract-client/build.gradle

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,23 @@ task buildTypescriptAngularSdk(type: org.openapitools.generator.gradle.plugin.ta
9595
dependsOn(':fineract-provider:resolve')
9696
}
9797

98+
task buildTypescriptFetchSdk(type: org.openapitools.generator.gradle.plugin.tasks.GenerateTask) {
99+
generatorName = 'typescript-fetch'
100+
verbose = false
101+
validateSpec = false
102+
skipValidateSpec = true
103+
inputSpec = "file:///$swaggerFile"
104+
outputDir = "$buildDir/generated/typescript-fetch".toString()
105+
configOptions = [
106+
npmName: '@apache/fineract-client-fetch',
107+
npmVersion: '1.12.0-SNAPSHOT',
108+
typescriptThreePlus: 'true',
109+
supportsES6: 'true',
110+
withInterfaces: 'true'
111+
]
112+
dependsOn(':fineract-provider:resolve')
113+
}
114+
98115
task buildAsciidoc(type: org.openapitools.generator.gradle.plugin.tasks.GenerateTask){
99116
generatorName = 'asciidoc'
100117
verbose = false

fineract-investor/src/main/resources/jpa/static-weaving/module/fineract-investor/persistence.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@
113113
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanApprovedAmountHistory</class>
114114
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanCharge</class>
115115
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy</class>
116+
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanChargeTaxDetails</class>
116117
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanCollateralManagement</class>
117118
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanCreditAllocationRule</class>
118119
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails</class>

fineract-loan-origination/src/main/resources/jpa/static-weaving/module/fineract-loan-origination/persistence.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@
103103
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanApprovedAmountHistory</class>
104104
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanCharge</class>
105105
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy</class>
106+
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanChargeTaxDetails</class>
106107
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanCollateralManagement</class>
107108
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanCreditAllocationRule</class>
108109
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails</class>
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.fineract.portfolio.loanaccount.data;
20+
21+
import java.math.BigDecimal;
22+
import lombok.AllArgsConstructor;
23+
import lombok.Data;
24+
import lombok.NoArgsConstructor;
25+
26+
/**
27+
* Carries the pro-rated tax amount for a single TaxComponent when a LoanCharge is (partially) paid. Used to propagate
28+
* tax details from the domain layer to the accounting bridge.
29+
*/
30+
@Data
31+
@NoArgsConstructor
32+
@AllArgsConstructor
33+
public class ChargeTaxDetailDTO {
34+
35+
/** GL account to credit (tax liability account from TaxComponent.creditAccount). */
36+
private Long creditAccountId;
37+
38+
/** Pro-rated tax amount for this component in this payment. */
39+
private BigDecimal amount;
40+
}

fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanChargePaidByDTO.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
package org.apache.fineract.portfolio.loanaccount.data;
2020

2121
import java.math.BigDecimal;
22+
import java.util.ArrayList;
23+
import java.util.List;
2224
import lombok.AllArgsConstructor;
2325
import lombok.Data;
2426
import lombok.NoArgsConstructor;
@@ -33,5 +35,6 @@ public class LoanChargePaidByDTO {
3335
private Long loanChargeId;
3436
private BigDecimal amount;
3537
private Integer installmentNumber;
38+
private List<ChargeTaxDetailDTO> taxDetails = new ArrayList<>();
3639

3740
}

fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,9 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom<Long> {
108108
@Column(name = "amount_outstanding_derived", scale = 6, precision = 19, nullable = false)
109109
private BigDecimal amountOutstanding;
110110

111+
@Column(name = "tax_amount", scale = 6, precision = 19)
112+
private BigDecimal taxAmount = BigDecimal.ZERO;
113+
111114
@Column(name = "is_penalty", nullable = false)
112115
private boolean penaltyCharge = false;
113116

@@ -143,6 +146,9 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom<Long> {
143146
@OneToMany(mappedBy = "loanCharge", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
144147
private Set<LoanChargePaidBy> loanChargePaidBySet = new HashSet<>();
145148

149+
@OneToMany(mappedBy = "loanCharge", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
150+
private List<LoanChargeTaxDetails> taxDetails = new ArrayList<>();
151+
146152
public void markAsFullyPaid() {
147153
this.amountPaid = this.amount;
148154
this.amountOutstanding = BigDecimal.ZERO;
@@ -412,6 +418,10 @@ public Money getAmountWrittenOff(final MonetaryCurrency currency) {
412418
return Money.of(currency, this.amountWrittenOff);
413419
}
414420

421+
public Money getTaxAmount(final MonetaryCurrency currency) {
422+
return Money.of(currency, getTaxAmount());
423+
}
424+
415425
/**
416426
* @param incrementBy
417427
*
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.fineract.portfolio.loanaccount.domain;
20+
21+
import jakarta.persistence.Column;
22+
import jakarta.persistence.Entity;
23+
import jakarta.persistence.JoinColumn;
24+
import jakarta.persistence.ManyToOne;
25+
import jakarta.persistence.Table;
26+
import java.math.BigDecimal;
27+
import lombok.AllArgsConstructor;
28+
import lombok.Getter;
29+
import lombok.Setter;
30+
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
31+
import org.apache.fineract.portfolio.tax.domain.TaxComponent;
32+
33+
@AllArgsConstructor
34+
@Setter
35+
@Getter
36+
@Entity
37+
@Table(name = "m_loan_charge_tax_details")
38+
public class LoanChargeTaxDetails extends AbstractPersistableCustom<Long> {
39+
40+
@ManyToOne
41+
@JoinColumn(name = "loan_charge_id", nullable = false)
42+
private LoanCharge loanCharge;
43+
44+
@ManyToOne
45+
@JoinColumn(name = "tax_component_id", nullable = false)
46+
private TaxComponent taxComponent;
47+
48+
@Column(name = "amount", scale = 6, precision = 19, nullable = false)
49+
private BigDecimal amount;
50+
51+
public LoanChargeTaxDetails() {
52+
53+
}
54+
}

fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeService.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
4444
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
4545
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy;
46+
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargeTaxDetails;
4647
import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails;
4748
import org.apache.fineract.portfolio.loanaccount.domain.LoanEvent;
4849
import org.apache.fineract.portfolio.loanaccount.domain.LoanInstallmentCharge;
@@ -56,6 +57,10 @@
5657
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
5758
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
5859
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
60+
import org.apache.fineract.portfolio.tax.domain.TaxComponent;
61+
import org.apache.fineract.portfolio.tax.domain.TaxGroup;
62+
import org.apache.fineract.portfolio.tax.service.ChargeTaxApplicationService;
63+
import org.apache.fineract.portfolio.tax.service.TaxUtils;
5964

6065
@RequiredArgsConstructor
6166
public class LoanChargeService {
@@ -65,6 +70,7 @@ public class LoanChargeService {
6570
private final LoanLifecycleStateMachine loanLifecycleStateMachine;
6671
private final LoanBalanceService loanBalanceService;
6772
private final LoanScheduleGeneratorService loanScheduleGeneratorService;
73+
private final ChargeTaxApplicationService chargeTaxApplicationService;
6874

6975
public void recalculateAllCharges(final Loan loan) {
7076
Set<LoanCharge> charges = loan.getActiveCharges();
@@ -393,6 +399,7 @@ public Map<String, Object> update(final JsonCommand command, final BigDecimal am
393399
break;
394400
}
395401
loanCharge.setAmountOrPercentage(newValue);
402+
applyTaxIfConfigured(loanCharge);
396403
if (loanCharge.isInstalmentFee()) {
397404
updateInstallmentCharges(loanCharge);
398405
}
@@ -445,11 +452,30 @@ public void populateDerivedFields(final LoanCharge loanCharge, final BigDecimal
445452
break;
446453
}
447454
loanCharge.setAmountOrPercentage(chargeAmount);
455+
applyTaxIfConfigured(loanCharge);
448456
if (loanCharge.getLoan() != null && loanCharge.isInstalmentFee()) {
449457
updateInstallmentCharges(loanCharge);
450458
}
451459
}
452460

461+
private void applyTaxIfConfigured(final LoanCharge loanCharge) {
462+
TaxGroup taxGroup = loanCharge.getCharge().getTaxGroup();
463+
if (taxGroup == null || loanCharge.getAmount() == null) {
464+
return;
465+
}
466+
LocalDate effectiveDate = loanCharge.getSubmittedOnDate() != null ? loanCharge.getSubmittedOnDate()
467+
: DateUtils.getBusinessLocalDate();
468+
Map<TaxComponent, BigDecimal> taxSplit = chargeTaxApplicationService.computeTax(taxGroup, loanCharge.getAmount(), effectiveDate, 6);
469+
BigDecimal totalTax = TaxUtils.totalTaxAmount(taxSplit);
470+
if (totalTax.compareTo(BigDecimal.ZERO) == 0) {
471+
return;
472+
}
473+
loanCharge.setTaxAmount(totalTax);
474+
loanCharge.setAmountOutstanding(loanCharge.calculateOutstanding());
475+
loanCharge.getTaxDetails().clear();
476+
taxSplit.forEach((component, taxAmt) -> loanCharge.getTaxDetails().add(new LoanChargeTaxDetails(loanCharge, component, taxAmt)));
477+
}
478+
453479
public void update(final LoanCharge loanCharge, final BigDecimal amount, final LocalDate dueDate, final Integer numberOfRepayments) {
454480
BigDecimal amountPercentageAppliedTo = BigDecimal.ZERO;
455481
if (loanCharge.getLoan() != null) {
@@ -814,6 +840,7 @@ private void update(final LoanCharge loanCharge, final BigDecimal amount, final
814840
break;
815841
}
816842
loanCharge.setAmountOrPercentage(amount);
843+
applyTaxIfConfigured(loanCharge);
817844
loanCharge.setAmountOutstanding(loanCharge.calculateOutstanding());
818845
if (loanCharge.getLoan() != null && loanCharge.isInstalmentFee()) {
819846
updateInstallmentCharges(loanCharge);
@@ -854,6 +881,7 @@ private void update(final LoanCharge loanCharge, final BigDecimal amount, final
854881
break;
855882
}
856883
loanCharge.setAmountOrPercentage(amount);
884+
applyTaxIfConfigured(loanCharge);
857885
loanCharge.setAmountOutstanding(loanCharge.calculateOutstanding());
858886
if (loanCharge.getLoan() != null && loanCharge.isInstalmentFee()) {
859887
updateInstallmentCharges(loanCharge, transactionDate);

fineract-loan/src/main/resources/db/changelog/tenant/module/loan/module-changelog-master.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,5 @@
5757
<include relativeToChangelogFile="true" file="parts/1032_add_classification_to_loan_transaction.xml"/>
5858
<include relativeToChangelogFile="true" file="parts/1033_add_reage_reasons_for_loan.xml"/>
5959
<include relativeToChangelogFile="true" file="parts/1034_loan_reamortization_parameters.xml"/>
60+
<include relativeToChangelogFile="true" file="parts/1035_add_tax_to_loan_charge.xml"/>
6061
</databaseChangeLog>
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
4+
Licensed to the Apache Software Foundation (ASF) under one
5+
or more contributor license agreements. See the NOTICE file
6+
distributed with this work for additional information
7+
regarding copyright ownership. The ASF licenses this file
8+
to you under the Apache License, Version 2.0 (the
9+
"License"); you may not use this file except in compliance
10+
with the License. You may obtain a copy of the License at
11+
12+
http://www.apache.org/licenses/LICENSE-2.0
13+
14+
Unless required by applicable law or agreed to in writing,
15+
software distributed under the License is distributed on an
16+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17+
KIND, either express or implied. See the License for the
18+
specific language governing permissions and limitations
19+
under the License.
20+
21+
-->
22+
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
23+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
24+
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
25+
<changeSet author="fineract" id="loan-charge-tax-1">
26+
<addColumn tableName="m_loan_charge">
27+
<column name="tax_amount" type="DECIMAL(19,6)">
28+
<constraints nullable="true"/>
29+
</column>
30+
</addColumn>
31+
</changeSet>
32+
33+
<changeSet author="fineract" id="loan-charge-tax-2">
34+
<createTable tableName="m_loan_charge_tax_details">
35+
<column autoIncrement="true" name="id" type="BIGINT">
36+
<constraints nullable="false" primaryKey="true"/>
37+
</column>
38+
<column name="loan_charge_id" type="BIGINT">
39+
<constraints nullable="false"/>
40+
</column>
41+
<column name="tax_component_id" type="BIGINT">
42+
<constraints nullable="false"/>
43+
</column>
44+
<column name="amount" type="DECIMAL(19,6)">
45+
<constraints nullable="false"/>
46+
</column>
47+
</createTable>
48+
<addForeignKeyConstraint constraintName="fk_loan_charge_tax_details_charge"
49+
baseTableName="m_loan_charge_tax_details" baseColumnNames="loan_charge_id"
50+
referencedTableName="m_loan_charge" referencedColumnNames="id"/>
51+
<addForeignKeyConstraint constraintName="fk_loan_charge_tax_details_component"
52+
baseTableName="m_loan_charge_tax_details" baseColumnNames="tax_component_id"
53+
referencedTableName="m_tax_component" referencedColumnNames="id"/>
54+
</changeSet>
55+
</databaseChangeLog>

0 commit comments

Comments
 (0)