feat(investment): bank-wise TDS on FD interest, GOI interest to the paisa and FD preview fixes
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Waiting to run
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Waiting to run
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Waiting to run
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Waiting to run
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Waiting to run
Fineract Documentation build / build (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Waiting to run
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Waiting to run
Fineract Liquibase Only mode - PostgreSQL / test (push) Waiting to run
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Waiting to run
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Waiting to run
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Waiting to run
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Waiting to run
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Waiting to run
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Waiting to run
Fineract Documentation build / build (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Waiting to run
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Waiting to run
Fineract Liquibase Only mode - PostgreSQL / test (push) Waiting to run
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Waiting to run
This commit is contained in:
+3
-1
@@ -74,6 +74,8 @@ public final class InvestmentApiConstants {
|
|||||||
public static final String CP_BRANCH_NAME = "cpBranchName";
|
public static final String CP_BRANCH_NAME = "cpBranchName";
|
||||||
public static final String CP_ACCOUNT_NO = "cpAccountNo";
|
public static final String CP_ACCOUNT_NO = "cpAccountNo";
|
||||||
public static final String CP_IFSC = "cpIfsc";
|
public static final String CP_IFSC = "cpIfsc";
|
||||||
|
public static final String TDS_APPLICABLE = "tdsApplicable";
|
||||||
|
public static final String TDS_RATE = "tdsRate";
|
||||||
|
|
||||||
// Script parameters
|
// Script parameters
|
||||||
public static final String SLR_TYPE = "slrType";
|
public static final String SLR_TYPE = "slrType";
|
||||||
@@ -88,7 +90,7 @@ public final class InvestmentApiConstants {
|
|||||||
|
|
||||||
public static final Set<String> PARTY_PARAMS = Set.of(BOND_TYPE, PARTY_NAME, ADDRESS1, ADDRESS2, ADDRESS3, CITY, PINCODE, PHONE, FAX,
|
public static final Set<String> PARTY_PARAMS = Set.of(BOND_TYPE, PARTY_NAME, ADDRESS1, ADDRESS2, ADDRESS3, CITY, PINCODE, PHONE, FAX,
|
||||||
BANK_TYPE, GL_ACCOUNT_ID, INTEREST_GL_ACCOUNT_ID, RBI_CODE, EXPOSURE_LIMIT, LEI_NO, CP_BANK_NAME, CP_BRANCH_NAME, CP_ACCOUNT_NO, CP_IFSC, ACTIVE,
|
BANK_TYPE, GL_ACCOUNT_ID, INTEREST_GL_ACCOUNT_ID, RBI_CODE, EXPOSURE_LIMIT, LEI_NO, CP_BANK_NAME, CP_BRANCH_NAME, CP_ACCOUNT_NO, CP_IFSC, ACTIVE,
|
||||||
LOCALE);
|
TDS_APPLICABLE, TDS_RATE, LOCALE);
|
||||||
|
|
||||||
public static final Set<String> SCRIPT_PARAMS = Set.of(BOND_TYPE, SLR_TYPE, SCRIPT_DESC, GL_ACCOUNT_ID, ISIN, FACE_VALUE_PER_UNIT,
|
public static final Set<String> SCRIPT_PARAMS = Set.of(BOND_TYPE, SLR_TYPE, SCRIPT_DESC, GL_ACCOUNT_ID, ISIN, FACE_VALUE_PER_UNIT,
|
||||||
INT_RATE, ISSUE_DATE, MATURITY_DATE, COMPANY_NAME, ADDRESS1, ADDRESS2, ADDRESS3, CITY, PINCODE, PHONE, FAX, CORP_CODE,
|
INT_RATE, ISSUE_DATE, MATURITY_DATE, COMPANY_NAME, ADDRESS1, ADDRESS2, ADDRESS3, CITY, PINCODE, PHONE, FAX, CORP_CODE,
|
||||||
|
|||||||
+16
-3
@@ -39,6 +39,7 @@ import org.apache.fineract.commands.domain.CommandWrapper;
|
|||||||
import org.apache.fineract.commands.service.CommandWrapperBuilder;
|
import org.apache.fineract.commands.service.CommandWrapperBuilder;
|
||||||
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
|
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
||||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
import org.apache.fineract.portfolio.investment.data.InvestmentFdClosureData;
|
import org.apache.fineract.portfolio.investment.data.InvestmentFdClosureData;
|
||||||
import org.apache.fineract.portfolio.investment.data.InvestmentFdData;
|
import org.apache.fineract.portfolio.investment.data.InvestmentFdData;
|
||||||
@@ -135,9 +136,13 @@ public class InvestmentFdApiResource {
|
|||||||
@Operation(summary = "Preview a maturity, premature closure or renewal", description = "type MATURITY | PREMATURE | RENEWAL")
|
@Operation(summary = "Preview a maturity, premature closure or renewal", description = "type MATURITY | PREMATURE | RENEWAL")
|
||||||
public InvestmentFdClosureData closureQuote(@PathParam("fdId") final Long fdId, @QueryParam("type") final String type,
|
public InvestmentFdClosureData closureQuote(@PathParam("fdId") final Long fdId, @QueryParam("type") final String type,
|
||||||
@QueryParam("closeDate") final String closeDate, @QueryParam("receivedAmount") final BigDecimal receivedAmount,
|
@QueryParam("closeDate") final String closeDate, @QueryParam("receivedAmount") final BigDecimal receivedAmount,
|
||||||
@QueryParam("tds") final BigDecimal tds, @QueryParam("renewalAmount") final BigDecimal renewalAmount) {
|
@QueryParam("tds") final BigDecimal tds, @QueryParam("renewalAmount") final BigDecimal renewalAmount,
|
||||||
|
@QueryParam("principalBankGlAccountId") final Long principalBankGlAccountId,
|
||||||
|
@QueryParam("interestBankGlAccountId") final Long interestBankGlAccountId,
|
||||||
|
@QueryParam("diffBankGlAccountId") final Long diffBankGlAccountId) {
|
||||||
this.context.authenticatedUser().validateHasReadPermission(InvestmentFdApiConstants.ENTITY);
|
this.context.authenticatedUser().validateHasReadPermission(InvestmentFdApiConstants.ENTITY);
|
||||||
return this.closureService.quote(fdId, type, date(closeDate), receivedAmount, tds, renewalAmount);
|
return this.closureService.quote(fdId, type, date(closeDate), receivedAmount, tds, renewalAmount, principalBankGlAccountId,
|
||||||
|
interestBankGlAccountId, diffBankGlAccountId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
@@ -181,6 +186,14 @@ public class InvestmentFdApiResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static LocalDate date(final String value) {
|
private static LocalDate date(final String value) {
|
||||||
return StringUtils.isBlank(value) ? null : LocalDate.parse(value.trim(), DATE);
|
if (StringUtils.isBlank(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return LocalDate.parse(value.trim(), DATE);
|
||||||
|
} catch (final java.time.format.DateTimeParseException e) {
|
||||||
|
// A half-typed date is a bad request, not a server error.
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.investmentfd.date.invalid", "Enter the date as DD-MM-YYYY", value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-1
@@ -39,6 +39,8 @@ public class InvestmentFdData {
|
|||||||
private final Long partyId;
|
private final Long partyId;
|
||||||
private final String partyCode;
|
private final String partyCode;
|
||||||
private final String partyName;
|
private final String partyName;
|
||||||
|
/** TDS rate the bank deducts on interest; null when it does not deduct TDS. */
|
||||||
|
private final BigDecimal tdsRate;
|
||||||
private final String certNo;
|
private final String certNo;
|
||||||
private final String certDate;
|
private final String certDate;
|
||||||
private final String dealDate;
|
private final String dealDate;
|
||||||
@@ -76,7 +78,9 @@ public class InvestmentFdData {
|
|||||||
|
|
||||||
public static InvestmentFdData from(final InvestmentFd f, final BigDecimal periodInterest) {
|
public static InvestmentFdData from(final InvestmentFd f, final BigDecimal periodInterest) {
|
||||||
return builder().id(f.getId()).dealNo(f.getDealNo()).partyId(f.getParty().getId()).partyCode(f.getParty().getPartyCode())
|
return builder().id(f.getId()).dealNo(f.getDealNo()).partyId(f.getParty().getId()).partyCode(f.getParty().getPartyCode())
|
||||||
.partyName(f.getParty().getPartyName()).certNo(f.getCertNo()).certDate(format(f.getCertDate()))
|
.partyName(f.getParty().getPartyName())
|
||||||
|
.tdsRate(f.getParty().isTdsApplicable() ? f.getParty().getTdsRate() : null).certNo(f.getCertNo())
|
||||||
|
.certDate(format(f.getCertDate()))
|
||||||
.dealDate(format(f.getDealDate())).principal(f.getPrincipal()).intRate(f.getIntRate())
|
.dealDate(format(f.getDealDate())).principal(f.getPrincipal()).intRate(f.getIntRate())
|
||||||
.maturityDate(format(f.getMaturityDate())).days(ChronoUnit.DAYS.between(f.getDealDate(), f.getMaturityDate()))
|
.maturityDate(format(f.getMaturityDate())).days(ChronoUnit.DAYS.between(f.getDealDate(), f.getMaturityDate()))
|
||||||
.payout(f.getPayout()).calcType(f.getCalcType()).expectedInterest(f.getExpectedInterest())
|
.payout(f.getPayout()).calcType(f.getCalcType()).expectedInterest(f.getExpectedInterest())
|
||||||
|
|||||||
+4
-1
@@ -45,6 +45,8 @@ public class InvestmentPartyData {
|
|||||||
private final Long interestGlAccountId;
|
private final Long interestGlAccountId;
|
||||||
private final String interestGlCode;
|
private final String interestGlCode;
|
||||||
private final String interestGlName;
|
private final String interestGlName;
|
||||||
|
private final boolean tdsApplicable;
|
||||||
|
private final BigDecimal tdsRate;
|
||||||
private final String rbiCode;
|
private final String rbiCode;
|
||||||
private final BigDecimal limit;
|
private final BigDecimal limit;
|
||||||
private final String leiNo;
|
private final String leiNo;
|
||||||
@@ -62,7 +64,8 @@ public class InvestmentPartyData {
|
|||||||
.interestGlAccountId(p.getInterestGlAccount() == null ? null : p.getInterestGlAccount().getId())
|
.interestGlAccountId(p.getInterestGlAccount() == null ? null : p.getInterestGlAccount().getId())
|
||||||
.interestGlCode(p.getInterestGlAccount() == null ? null : p.getInterestGlAccount().getGlCode())
|
.interestGlCode(p.getInterestGlAccount() == null ? null : p.getInterestGlAccount().getGlCode())
|
||||||
.interestGlName(p.getInterestGlAccount() == null ? null : p.getInterestGlAccount().getName())
|
.interestGlName(p.getInterestGlAccount() == null ? null : p.getInterestGlAccount().getName())
|
||||||
.rbiCode(p.getRbiCode()).limit(p.getExposureLimit()).leiNo(p.getLeiNo()).cpBankName(p.getCpBankName())
|
.tdsApplicable(p.isTdsApplicable()).tdsRate(p.getTdsRate()).rbiCode(p.getRbiCode()).limit(p.getExposureLimit())
|
||||||
|
.leiNo(p.getLeiNo()).cpBankName(p.getCpBankName())
|
||||||
.cpBranchName(p.getCpBranchName()).cpAccountNo(p.getCpAccountNo()).cpIfsc(p.getCpIfsc()).active(p.isActive()).build();
|
.cpBranchName(p.getCpBranchName()).cpAccountNo(p.getCpAccountNo()).cpIfsc(p.getCpIfsc()).active(p.isActive()).build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
@@ -83,6 +83,14 @@ public class InvestmentParty extends AbstractAuditableWithUTCDateTimeCustom<Long
|
|||||||
@JoinColumn(name = "interest_gl_account_id")
|
@JoinColumn(name = "interest_gl_account_id")
|
||||||
private GLAccount interestGlAccount;
|
private GLAccount interestGlAccount;
|
||||||
|
|
||||||
|
/** Whether this bank deducts TDS from the interest on FDs placed with it (some banks do, some do not). */
|
||||||
|
@Column(name = "tds_applicable", nullable = false)
|
||||||
|
private boolean tdsApplicable;
|
||||||
|
|
||||||
|
/** TDS rate in per cent, set only when the bank deducts TDS. */
|
||||||
|
@Column(name = "tds_rate", scale = 4, precision = 9)
|
||||||
|
private BigDecimal tdsRate;
|
||||||
|
|
||||||
@Column(name = "rbi_code", length = 30)
|
@Column(name = "rbi_code", length = 30)
|
||||||
private String rbiCode;
|
private String rbiCode;
|
||||||
|
|
||||||
|
|||||||
+5
@@ -53,6 +53,7 @@ import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstant
|
|||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SCRIPT_PARAMS;
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SCRIPT_PARAMS;
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SLR_TYPE;
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SLR_TYPE;
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SLR_TYPES;
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SLR_TYPES;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.TDS_RATE;
|
||||||
|
|
||||||
import com.google.gson.JsonElement;
|
import com.google.gson.JsonElement;
|
||||||
import com.google.gson.reflect.TypeToken;
|
import com.google.gson.reflect.TypeToken;
|
||||||
@@ -122,6 +123,10 @@ public final class InvestmentMasterCommandFromApiJsonDeserializer {
|
|||||||
final BigDecimal limit = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(EXPOSURE_LIMIT, element);
|
final BigDecimal limit = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(EXPOSURE_LIMIT, element);
|
||||||
v.reset().parameter(EXPOSURE_LIMIT).value(limit).ignoreIfNull().zeroOrPositiveAmount();
|
v.reset().parameter(EXPOSURE_LIMIT).value(limit).ignoreIfNull().zeroOrPositiveAmount();
|
||||||
}
|
}
|
||||||
|
if (this.fromApiJsonHelper.parameterExists(TDS_RATE, element)) {
|
||||||
|
final BigDecimal rate = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(TDS_RATE, element);
|
||||||
|
v.reset().parameter(TDS_RATE).value(rate).ignoreIfNull().positiveAmount().notGreaterThanMax(BigDecimal.valueOf(100));
|
||||||
|
}
|
||||||
throwIfErrors(errors);
|
throwIfErrors(errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+11
-3
@@ -111,12 +111,17 @@ public class InvestmentFdClosureService {
|
|||||||
/** Preview of a closure; nothing is posted. */
|
/** Preview of a closure; nothing is posted. */
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public InvestmentFdClosureData quote(final Long fdId, final String type, final LocalDate closeDate, final BigDecimal receivedAmount,
|
public InvestmentFdClosureData quote(final Long fdId, final String type, final LocalDate closeDate, final BigDecimal receivedAmount,
|
||||||
final BigDecimal tds, final BigDecimal renewalAmount) {
|
final BigDecimal tds, final BigDecimal renewalAmount, final Long principalBankGlId, final Long interestBankGlId,
|
||||||
|
final Long diffBankGlId) {
|
||||||
final InvestmentFd f = this.fdRepository.findById(fdId).orElseThrow(() -> InvestmentFdService.rule("not.found", "FD " + fdId + " does not exist"));
|
final InvestmentFd f = this.fdRepository.findById(fdId).orElseThrow(() -> InvestmentFdService.rule("not.found", "FD " + fdId + " does not exist"));
|
||||||
final String t = CLOSURE_PREMATURE.equals(type) || CLOSURE_RENEWAL.equals(type) ? type : CLOSURE_MATURITY;
|
final String t = CLOSURE_PREMATURE.equals(type) || CLOSURE_RENEWAL.equals(type) ? type : CLOSURE_MATURITY;
|
||||||
final LocalDate date = CLOSURE_PREMATURE.equals(t) ? (closeDate == null ? DateUtils.getBusinessLocalDate() : closeDate) : f.getMaturityDate();
|
final LocalDate date = CLOSURE_PREMATURE.equals(t) ? (closeDate == null ? DateUtils.getBusinessLocalDate() : closeDate) : f.getMaturityDate();
|
||||||
final Figures x = figures(f, t, date, receivedAmount, tds, renewalAmount);
|
final Figures x = figures(f, t, date, receivedAmount, tds, renewalAmount);
|
||||||
final Entry e = CLOSURE_RENEWAL.equals(t) ? renewalEntry(f, x, f.getBankGlAccount()) : closureEntry(f, x, f.getBankGlAccount(), f.getBankGlAccount());
|
// The banks the user has chosen, as the posting will use them; a renewal difference shows no bank line until one is chosen.
|
||||||
|
final GLAccount principalGl = principalBankGlId == null ? f.getBankGlAccount() : bankGl(principalBankGlId, f);
|
||||||
|
final GLAccount interestGl = interestBankGlId == null ? principalGl : bankGl(interestBankGlId, f);
|
||||||
|
final Entry e = CLOSURE_RENEWAL.equals(t) ? renewalEntry(f, x, diffBankGlId == null ? null : bankGl(diffBankGlId, f))
|
||||||
|
: closureEntry(f, x, principalGl, interestGl);
|
||||||
return x.toData(f, t, date, null, e);
|
return x.toData(f, t, date, null, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,7 +333,10 @@ public class InvestmentFdClosureService {
|
|||||||
}
|
}
|
||||||
final BigDecimal expected = InvestmentFdCalculator.interest(f.getPrincipal(), f.getIntRate(), lastPaid, date, f.getCalcType(),
|
final BigDecimal expected = InvestmentFdCalculator.interest(f.getPrincipal(), f.getIntRate(), lastPaid, date, f.getCalcType(),
|
||||||
f.getPayout());
|
f.getPayout());
|
||||||
final BigDecimal tds = scale(zero(tdsIn));
|
// TDS as entered; otherwise worked out at the bank's TDS rate, from the bank's net figure when given.
|
||||||
|
final BigDecimal tds = tdsIn != null ? scale(tdsIn)
|
||||||
|
: amountIn != null ? InvestmentFdService.tdsInNet(f.getParty(), scale(amountIn).subtract(f.getPrincipal()))
|
||||||
|
: InvestmentFdService.tdsOnGross(f.getParty(), expected);
|
||||||
// Maturity / renewal default to the system's figure; a premature closure always needs the bank's amount.
|
// Maturity / renewal default to the system's figure; a premature closure always needs the bank's amount.
|
||||||
final BigDecimal received = amountIn != null ? scale(amountIn) : f.getPrincipal().add(expected).subtract(tds);
|
final BigDecimal received = amountIn != null ? scale(amountIn) : f.getPrincipal().add(expected).subtract(tds);
|
||||||
final BigDecimal interest = received.add(tds).subtract(f.getPrincipal());
|
final BigDecimal interest = received.add(tds).subtract(f.getPrincipal());
|
||||||
|
|||||||
+11
-6
@@ -176,11 +176,15 @@ public class InvestmentFdInterestService {
|
|||||||
x.setAmount(next.amount());
|
x.setAmount(next.amount());
|
||||||
what = "FD interest provision";
|
what = "FD interest provision";
|
||||||
} else {
|
} else {
|
||||||
// The bank's own figure when given (net of TDS), else the system's interest for the period.
|
// The bank's own figure when given (net of TDS), else the system's interest for the period. TDS as entered, else
|
||||||
final BigDecimal tds = amount(command, TDS);
|
// worked out at the bank's TDS rate.
|
||||||
final BigDecimal received = command.parameterExists(RECEIVED_AMOUNT) && command.bigDecimalValueOfParameterNamed(RECEIVED_AMOUNT) != null
|
final boolean bankFigure = command.parameterExists(RECEIVED_AMOUNT)
|
||||||
? amount(command, RECEIVED_AMOUNT)
|
&& command.bigDecimalValueOfParameterNamed(RECEIVED_AMOUNT) != null;
|
||||||
: next.gross().subtract(tds);
|
final boolean tdsGiven = command.parameterExists(TDS) && command.bigDecimalValueOfParameterNamed(TDS) != null;
|
||||||
|
final BigDecimal tds = tdsGiven ? amount(command, TDS)
|
||||||
|
: bankFigure ? InvestmentFdService.tdsInNet(f.getParty(), amount(command, RECEIVED_AMOUNT))
|
||||||
|
: InvestmentFdService.tdsOnGross(f.getParty(), next.gross());
|
||||||
|
final BigDecimal received = bankFigure ? amount(command, RECEIVED_AMOUNT) : next.gross().subtract(tds);
|
||||||
final BigDecimal gross = received.add(tds);
|
final BigDecimal gross = received.add(tds);
|
||||||
final BigDecimal income = gross.subtract(next.alreadyBooked());
|
final BigDecimal income = gross.subtract(next.alreadyBooked());
|
||||||
final GLAccount bank = command.longValueOfParameterNamed(BANK_GL_ACCOUNT_ID) == null ? f.getBankGlAccount()
|
final GLAccount bank = command.longValueOfParameterNamed(BANK_GL_ACCOUNT_ID) == null ? f.getBankGlAccount()
|
||||||
@@ -260,7 +264,8 @@ public class InvestmentFdInterestService {
|
|||||||
final BigDecimal gross = done != null ? done.getGrossInterest()
|
final BigDecimal gross = done != null ? done.getGrossInterest()
|
||||||
: InvestmentFdCalculator.interest(f.getPrincipal(), f.getIntRate(), lastPaid, d, CALC_SIMPLE, f.getPayout());
|
: InvestmentFdCalculator.interest(f.getPrincipal(), f.getIntRate(), lastPaid, d, CALC_SIMPLE, f.getPayout());
|
||||||
final BigDecimal booked = done != null ? zero(done.getAlreadyBooked()) : sincePaid;
|
final BigDecimal booked = done != null ? zero(done.getAlreadyBooked()) : sincePaid;
|
||||||
out.add(new Line(type, d, lastPaid, gross, booked, done == null ? null : done.getTds(),
|
final BigDecimal tds = done == null ? InvestmentFdService.tdsOnGross(f.getParty(), gross) : done.getTds();
|
||||||
|
out.add(new Line(type, d, lastPaid, gross, booked, tds,
|
||||||
done != null ? done.getAmount() : gross.subtract(booked), done));
|
done != null ? done.getAmount() : gross.subtract(booked), done));
|
||||||
lastPaid = d;
|
lastPaid = d;
|
||||||
sincePaid = BigDecimal.ZERO;
|
sincePaid = BigDecimal.ZERO;
|
||||||
|
|||||||
+19
@@ -287,6 +287,25 @@ public class InvestmentFdService {
|
|||||||
return PAYOUT_AT_MATURITY.equals(payout) ? amount.add(interest) : amount;
|
return PAYOUT_AT_MATURITY.equals(payout) ? amount.add(interest) : amount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** TDS the bank deducts from a gross interest at the rate on its FD party; zero when the bank does not deduct TDS. */
|
||||||
|
static BigDecimal tdsOnGross(final InvestmentParty party, final BigDecimal grossInterest) {
|
||||||
|
if (!party.isTdsApplicable() || party.getTdsRate() == null || grossInterest == null || grossInterest.signum() <= 0) {
|
||||||
|
return BigDecimal.ZERO;
|
||||||
|
}
|
||||||
|
return grossInterest.multiply(party.getTdsRate()).divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** TDS already taken out of an interest the bank paid net of it: gross = net × 100 / (100 − rate), TDS = gross − net. */
|
||||||
|
static BigDecimal tdsInNet(final InvestmentParty party, final BigDecimal netInterest) {
|
||||||
|
final BigDecimal hundred = BigDecimal.valueOf(100);
|
||||||
|
if (!party.isTdsApplicable() || party.getTdsRate() == null || party.getTdsRate().compareTo(hundred) >= 0 || netInterest == null
|
||||||
|
|| netInterest.signum() <= 0) {
|
||||||
|
return BigDecimal.ZERO;
|
||||||
|
}
|
||||||
|
final BigDecimal gross = netInterest.multiply(hundred).divide(hundred.subtract(party.getTdsRate()), 2, RoundingMode.HALF_UP);
|
||||||
|
return gross.subtract(netInterest).setScale(2, RoundingMode.HALF_UP);
|
||||||
|
}
|
||||||
|
|
||||||
/** The 31 March of the previous financial year: for 16-10-2026 → 31-03-2026, for 31-03-2026 → 31-03-2025. */
|
/** The 31 March of the previous financial year: for 16-10-2026 → 31-03-2026, for 31-03-2026 → 31-03-2025. */
|
||||||
static LocalDate previousYearEnd(final LocalDate date) {
|
static LocalDate previousYearEnd(final LocalDate date) {
|
||||||
final int year = date.getMonthValue() >= 4 ? date.getYear() : date.getYear() - 1;
|
final int year = date.getMonthValue() >= 4 ? date.getYear() : date.getYear() - 1;
|
||||||
|
|||||||
+4
-4
@@ -66,14 +66,14 @@ public final class InvestmentInterestCalculator {
|
|||||||
return (to.getYear() - from.getYear()) * 360 + (to.getMonthValue() - from.getMonthValue()) * 30 + (d2 - d1);
|
return (to.getYear() - from.getYear()) * 360 + (to.getMonthValue() - from.getMonthValue()) * 30 + (d2 - d1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Interest on {@code faceValue} at {@code ratePercent} p.a. for {@code days} (30/360), rounded to the rupee. */
|
/** Interest on {@code faceValue} at {@code ratePercent} p.a. for {@code days} (30/360), to the paisa. */
|
||||||
public static BigDecimal interestForDays(final BigDecimal faceValue, final BigDecimal ratePercent, final int days) {
|
public static BigDecimal interestForDays(final BigDecimal faceValue, final BigDecimal ratePercent, final int days) {
|
||||||
return faceValue.multiply(ratePercent).multiply(BigDecimal.valueOf(days)).divide(THIRTY_SIX_THOUSAND, 0, RoundingMode.HALF_UP);
|
return faceValue.multiply(ratePercent).multiply(BigDecimal.valueOf(days)).divide(THIRTY_SIX_THOUSAND, 2, RoundingMode.HALF_UP);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The fixed half-yearly coupon: face value × rate ÷ 2. */
|
/** The fixed half-yearly coupon: face value × rate ÷ 2, to the paisa. */
|
||||||
public static BigDecimal halfYearCoupon(final BigDecimal faceValue, final BigDecimal ratePercent) {
|
public static BigDecimal halfYearCoupon(final BigDecimal faceValue, final BigDecimal ratePercent) {
|
||||||
return faceValue.multiply(ratePercent).divide(TWO_HUNDRED, 0, RoundingMode.HALF_UP);
|
return faceValue.multiply(ratePercent).divide(TWO_HUNDRED, 2, RoundingMode.HALF_UP);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** k-th coupon date counting back from maturity — measured from maturity each time so month-end days survive. */
|
/** k-th coupon date counting back from maturity — measured from maturity each time so month-end days survive. */
|
||||||
|
|||||||
+15
@@ -49,6 +49,8 @@ import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstant
|
|||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SCRIPT_CODE_PREFIX;
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SCRIPT_CODE_PREFIX;
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SCRIPT_DESC;
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SCRIPT_DESC;
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SLR_TYPE;
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SLR_TYPE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.TDS_APPLICABLE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.TDS_RATE;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -160,6 +162,19 @@ public class InvestmentMasterWritePlatformServiceImpl implements InvestmentMaste
|
|||||||
final Long interestGlId = command.longValueOfParameterNamed(INTEREST_GL_ACCOUNT_ID);
|
final Long interestGlId = command.longValueOfParameterNamed(INTEREST_GL_ACCOUNT_ID);
|
||||||
p.setInterestGlAccount(interestGlId == null ? null : postableIncomeGl(interestGlId));
|
p.setInterestGlAccount(interestGlId == null ? null : postableIncomeGl(interestGlId));
|
||||||
}
|
}
|
||||||
|
if (command.parameterExists(TDS_APPLICABLE)) {
|
||||||
|
p.setTdsApplicable(command.booleanPrimitiveValueOfParameterNamed(TDS_APPLICABLE));
|
||||||
|
}
|
||||||
|
if (command.parameterExists(TDS_RATE)) {
|
||||||
|
p.setTdsRate(command.bigDecimalValueOfParameterNamed(TDS_RATE));
|
||||||
|
}
|
||||||
|
// A bank that deducts TDS needs its rate; one that does not keeps no rate.
|
||||||
|
if (!p.isTdsApplicable()) {
|
||||||
|
p.setTdsRate(null);
|
||||||
|
} else if (p.getTdsRate() == null || p.getTdsRate().signum() <= 0) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.investment.party.tds.rate.required",
|
||||||
|
"Enter the TDS rate this bank deducts on FD interest");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Interest on an FD with this bank is credited here, so it must be a usable Income account. */
|
/** Interest on an FD with this bank is credited here, so it must be a usable Income account. */
|
||||||
|
|||||||
@@ -184,4 +184,5 @@
|
|||||||
<include file="parts/0602_investment_security_transfer.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0602_investment_security_transfer.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0603_investment_premium_amortisation.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0603_investment_premium_amortisation.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0604_investment_fd.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0604_investment_fd.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="parts/0605_investment_party_tds.xml" relativeToChangelogFile="true"/>
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
|
|||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!--
|
||||||
|
|
||||||
|
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.9.xsd">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Some banks deduct TDS from the interest on the bank's FDs with them and some do not, so the FD party says whether
|
||||||
|
it does and at what rate. The rate pre-fills the TDS on interest payouts and closures; the user can still change it.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<changeSet author="cbs-anshul" id="0605-1-investment-party-tds">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not><columnExists tableName="m_investment_party" columnName="tds_applicable"/></not>
|
||||||
|
</preConditions>
|
||||||
|
<addColumn tableName="m_investment_party">
|
||||||
|
<column name="tds_applicable" type="BOOLEAN" defaultValueBoolean="false">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="tds_rate" type="DECIMAL(9,4)"/>
|
||||||
|
</addColumn>
|
||||||
|
</changeSet>
|
||||||
|
</databaseChangeLog>
|
||||||
+12
-12
@@ -60,16 +60,16 @@ class InvestmentInterestCalculatorTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void interestForDaysRoundsToTheRupee() {
|
void interestForDaysIsToThePaisa() {
|
||||||
assertEquals(new BigDecimal("120833"), InvestmentInterestCalculator.interestForDays(ONE_CRORE, RATE, 58));
|
assertEquals(new BigDecimal("120833.33"), InvestmentInterestCalculator.interestForDays(ONE_CRORE, RATE, 58));
|
||||||
assertEquals(new BigDecimal("104167"), InvestmentInterestCalculator.interestForDays(ONE_CRORE, RATE, 50));
|
assertEquals(new BigDecimal("104166.67"), InvestmentInterestCalculator.interestForDays(ONE_CRORE, RATE, 50));
|
||||||
assertEquals(new BigDecimal("375000"), InvestmentInterestCalculator.interestForDays(ONE_CRORE, RATE, 180));
|
assertEquals(new BigDecimal("375000.00"), InvestmentInterestCalculator.interestForDays(ONE_CRORE, RATE, 180));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void halfYearCouponIsFaceValueTimesRateOverTwo() {
|
void halfYearCouponIsFaceValueTimesRateOverTwo() {
|
||||||
assertEquals(new BigDecimal("375000"), InvestmentInterestCalculator.halfYearCoupon(ONE_CRORE, RATE));
|
assertEquals(new BigDecimal("375000.00"), InvestmentInterestCalculator.halfYearCoupon(ONE_CRORE, RATE));
|
||||||
assertEquals(new BigDecimal("362500"), InvestmentInterestCalculator.halfYearCoupon(ONE_CRORE, new BigDecimal("7.25")));
|
assertEquals(new BigDecimal("362500.00"), InvestmentInterestCalculator.halfYearCoupon(ONE_CRORE, new BigDecimal("7.25")));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -106,14 +106,14 @@ class InvestmentInterestCalculatorTest {
|
|||||||
// 16 coupons + 15 closings (Mar 2027 … Mar 2034); 30-09-2026 is before the deal, so it is the seller's.
|
// 16 coupons + 15 closings (Mar 2027 … Mar 2034); 30-09-2026 is before the deal, so it is the seller's.
|
||||||
assertEquals(31, rows.size());
|
assertEquals(31, rows.size());
|
||||||
|
|
||||||
// First coupon is income in full: the 1,20,833 paid to the seller was booked as an expense at purchase (RBI
|
// First coupon is income in full: the 1,20,833.33 paid to the seller was booked as an expense at purchase (RBI
|
||||||
// para 152).
|
// para 152).
|
||||||
assertRow(rows.get(0), TYPE_COUPON, LocalDate.of(2027, 2, 10), LocalDate.of(2026, 8, 11), 180, "375000", "0", "375000");
|
assertRow(rows.get(0), TYPE_COUPON, LocalDate.of(2027, 2, 10), LocalDate.of(2026, 8, 11), 180, "375000.00", "0", "375000.00");
|
||||||
// 31 March closing: 50 days earned since 10 Feb, nothing booked yet.
|
// 31 March closing: 50 days earned since 10 Feb, nothing booked yet.
|
||||||
assertRow(rows.get(1), TYPE_PROVISION, LocalDate.of(2027, 3, 31), LocalDate.of(2027, 2, 11), 50, "104167", "0", "104167");
|
assertRow(rows.get(1), TYPE_PROVISION, LocalDate.of(2027, 3, 31), LocalDate.of(2027, 2, 11), 50, "104166.67", "0", "104166.67");
|
||||||
// August coupon clears the March provision.
|
// August coupon clears the March provision.
|
||||||
assertRow(rows.get(2), TYPE_COUPON, LocalDate.of(2027, 8, 10), LocalDate.of(2027, 2, 11), 180, "375000", "104167", "270833");
|
assertRow(rows.get(2), TYPE_COUPON, LocalDate.of(2027, 8, 10), LocalDate.of(2027, 2, 11), 180, "375000.00", "104166.67", "270833.33");
|
||||||
assertRow(rows.get(3), TYPE_PROVISION, LocalDate.of(2027, 9, 30), LocalDate.of(2027, 8, 11), 50, "104167", "0", "104167");
|
assertRow(rows.get(3), TYPE_PROVISION, LocalDate.of(2027, 9, 30), LocalDate.of(2027, 8, 11), 50, "104166.67", "0", "104166.67");
|
||||||
|
|
||||||
final ScheduleRow last = rows.get(rows.size() - 1);
|
final ScheduleRow last = rows.get(rows.size() - 1);
|
||||||
assertEquals(TYPE_COUPON, last.type());
|
assertEquals(TYPE_COUPON, last.type());
|
||||||
@@ -121,7 +121,7 @@ class InvestmentInterestCalculatorTest {
|
|||||||
|
|
||||||
// Over the life, income booked = all the coupons.
|
// Over the life, income booked = all the coupons.
|
||||||
final BigDecimal totalIncome = rows.stream().map(ScheduleRow::amount).reduce(BigDecimal.ZERO, BigDecimal::add);
|
final BigDecimal totalIncome = rows.stream().map(ScheduleRow::amount).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
assertEquals(new BigDecimal("375000").multiply(BigDecimal.valueOf(16)), totalIncome);
|
assertEquals(0, new BigDecimal("375000").multiply(BigDecimal.valueOf(16)).compareTo(totalIncome));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Reference in New Issue
Block a user